From da99cd61920969945805c1f6ebcb624b1992baeb Mon Sep 17 00:00:00 2001 From: jfkirk Date: Tue, 24 May 2016 14:26:44 -0400 Subject: [PATCH] ENH: Adds BMF, LSE, and TSX exchange calendars --- tests/test_exchange_calendar.py | 2 +- zipline/utils/calendars/exchange_calendar.py | 2 +- .../utils/calendars/exchange_calendar_bmf.py | 376 ++++++++++++++++++ .../utils/calendars/exchange_calendar_lse.py | 287 +++++++++++++ ..._calendar.py => exchange_calendar_nyse.py} | 2 +- .../utils/calendars/exchange_calendar_tsx.py | 291 ++++++++++++++ 6 files changed, 957 insertions(+), 3 deletions(-) create mode 100644 zipline/utils/calendars/exchange_calendar_bmf.py create mode 100644 zipline/utils/calendars/exchange_calendar_lse.py rename zipline/utils/calendars/{nyse_exchange_calendar.py => exchange_calendar_nyse.py} (99%) create mode 100644 zipline/utils/calendars/exchange_calendar_tsx.py diff --git a/tests/test_exchange_calendar.py b/tests/test_exchange_calendar.py index cf08de87..d21a1f29 100644 --- a/tests/test_exchange_calendar.py +++ b/tests/test_exchange_calendar.py @@ -31,7 +31,7 @@ from pandas import ( ) from pandas.util.testing import assert_frame_equal -from zipline.utils.calendars.nyse_exchange_calendar import NYSEExchangeCalendar +from zipline.utils.calendars.exchange_calendar_nyse import NYSEExchangeCalendar class ExchangeCalendarTestBase(object): diff --git a/zipline/utils/calendars/exchange_calendar.py b/zipline/utils/calendars/exchange_calendar.py index 5d832234..9b1066c6 100644 --- a/zipline/utils/calendars/exchange_calendar.py +++ b/zipline/utils/calendars/exchange_calendar.py @@ -482,7 +482,7 @@ def get_calendar(name): raise InvalidCalendarName(calendar_name=name) if name == 'NYSE': - from zipline.utils.calendars.nyse_exchange_calendar \ + from zipline.utils.calendars.exchange_calendar_nyse \ import NYSEExchangeCalendar nyse_cal = NYSEExchangeCalendar() register_calendar(nyse_cal) diff --git a/zipline/utils/calendars/exchange_calendar_bmf.py b/zipline/utils/calendars/exchange_calendar_bmf.py new file mode 100644 index 00000000..9831a9e7 --- /dev/null +++ b/zipline/utils/calendars/exchange_calendar_bmf.py @@ -0,0 +1,376 @@ +from datetime import time +from pandas import Timedelta +from pandas.tseries.holiday import( + AbstractHolidayCalendar, + Holiday, + Easter, + Day, + GoodFriday, +) +from pytz import timezone + +from zipline.utils.calendars.exchange_calendar import ExchangeCalendar +from zipline.utils.calendars.calendar_helpers import normalize_date + +MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY = range(7) + +# Universal Confraternization (new years day) +ConfUniversal = Holiday( + 'Dia da Confraternizacao Universal', + month=1, + day=1, +) +# Sao Paulo city birthday +AniversarioSaoPaulo = Holiday( + 'Aniversario de Sao Paulo', + month=1, + day=25, +) +# Carnival Monday +CarnavalSegunda = Holiday( + 'Carnaval Segunda', + month=1, + day=1, + offset=[Easter(), Day(-48)] +) +# Carnival Tuesday +CarnavalTerca = Holiday( + 'Carnaval Terca', + month=1, + day=1, + offset=[Easter(), Day(-47)] +) +# Ash Wednesday (short day) +QuartaCinzas = Holiday( + 'Quarta Cinzas', + month=1, + day=1, + offset=[Easter(), Day(-46)] +) +# Good Friday +SextaPaixao = GoodFriday +# Feast of the Most Holy Body of Christ +CorpusChristi = Holiday( + 'Corpus Christi', + month=1, + day=1, + offset=[Easter(), Day(60)] +) +# Tiradentes Memorial +Tiradentes = Holiday( + 'Tiradentes', + month=4, + day=21, +) +# Labor Day +DiaTrabalho = Holiday( + 'Dia Trabalho', + month=5, + day=1, +) +# Constitutionalist Revolution +Constitucionalista = Holiday( + 'Constitucionalista', + month=7, + day=9, + start_date='1997-01-01' +) +# Independence Day +Independencia = Holiday( + 'Independencia', + month=9, + day=7, +) +# Our Lady of Aparecida +Aparecida = Holiday( + 'Nossa Senhora de Aparecida', + month=10, + day=12, +) +# All Souls' Day +Finados = Holiday( + 'Dia dos Finados', + month=11, + day=2, +) +# Proclamation of the Republic +ProclamacaoRepublica = Holiday( + 'Proclamacao da Republica', + month=11, + day=15, +) +# Day of Black Awareness +ConscienciaNegra = Holiday( + 'Dia da Consciencia Negra', + month=11, + day=20, + start_date='2004-01-01' +) +# Christmas Eve +VesperaNatal = Holiday( + 'Vespera Natal', + month=12, + day=24, +) +# Christmas +Natal = Holiday( + 'Natal', + month=12, + day=25, +) +# New Year's Eve +AnoNovo = Holiday( + 'Ano Novo', + month=12, + day=31, +) +# New Year's Eve falls on Saturday +AnoNovoSabado = Holiday( + 'Ano Novo Sabado', + month=12, + day=30, + days_of_week=(FRIDAY), +) + + +class BMFHolidayCalendar(AbstractHolidayCalendar): + """ + Non-trading days for the BM&F. + + See NYSEExchangeCalendar for full description. + """ + rules = [ + ConfUniversal, + AniversarioSaoPaulo, + CarnavalSegunda, + CarnavalTerca, + SextaPaixao, + CorpusChristi, + Tiradentes, + DiaTrabalho, + Constitucionalista, + Independencia, + Aparecida, + Finados, + ProclamacaoRepublica, + ConscienciaNegra, + VesperaNatal, + Natal, + AnoNovo, + AnoNovoSabado, + ] + + +class BMFLateOpenCalendar(AbstractHolidayCalendar): + """ + Regular early close calendar for NYSE + """ + rules = [ + QuartaCinzas, + ] + + +class BMFExchangeCalendar(ExchangeCalendar): + """ + Exchange calendar for BM&F BOVESPA + + Open Time: 10:00 AM, Brazil/Sao Paulo + Close Time: 4:00 PM, Brazil/Sao Paulo + + Regularly-Observed Holidays: + - Universal Confraternization (New year's day, Jan 1) + - Sao Paulo City Anniversary (Jan 25) + - Carnaval Monday (48 days before Easter) + - Carnaval Tuesday (47 days before Easter) + - Passion of the Christ (Good Friday, 2 days before Easter) + - Corpus Christi (60 days after Easter) + - Tiradentes (April 21) + - Labor day (May 1) + - Constitutionalist Revolution (July 9 after 1997) + - Independence Day (September 7) + - Our Lady of Aparecida Feast (October 12) + - All Souls' Day (November 2) + - Proclamation of the Republic (November 15) + - Day of Black Awareness (November 20 after 2004) + - Christmas (December 24 and 25) + - Day before New Year's Eve (December 30 if NYE falls on a Saturday) + - New Year's Eve (December 31) + """ + + exchange_name = 'BMF' + native_timezone = timezone('America/Sao_Paulo') + open_time = time(10, 01) + close_time = time(17) + + # Does the market open or close on a different calendar day, compared to + # the calendar day assigned by the exchange to this session? + open_offset = 0 + close_offset = 0 + + holidays_calendar = BMFHolidayCalendar() + special_opens_calendars = [ + (time(13, 01), BMFLateOpenCalendar()), + ] + special_closes_calendars = () + + holidays_adhoc = () + + special_opens_adhoc = () + special_closes_adhoc = () + + @property + def name(self): + """ + The name of this exchange calendar. + E.g.: 'NYSE', 'LSE', 'CME Energy' + """ + return self.exchange_name + + @property + def tz(self): + """ + The native timezone of the exchange. + """ + return self.native_timezone + + def is_open_on_minute(self, dt): + """ + Is the exchange open (accepting orders) at @dt. + + Parameters + ---------- + dt : Timestamp + + Returns + ------- + bool + True if exchange is open at the given dt, otherwise False. + """ + # Retrieve the exchange session relevant for this datetime + session = self.session_date(dt) + # Retrieve the open and close for this exchange session + open, close = self.open_and_close(session) + # Is @dt within the trading hours for this exchange session + return open <= dt and dt <= close + + def is_open_on_day(self, dt): + """ + Is the exchange open (accepting orders) anytime during the calendar day + containing @dt. + + Parameters + ---------- + dt : Timestamp + + Returns + ------- + bool + True if exchange is open at any time during the day containing @dt + """ + dt_normalized = normalize_date(dt) + return dt_normalized in self.schedule.index + + def trading_days(self, start, end): + """ + Calculates all of the exchange sessions between the given + start and end, inclusive. + + SD: Should @start and @end are UTC-canonicalized, as our exchange + sessions are. If not, then it's not clear how this method should behave + if @start and @end are both in the middle of the day. Here, I assume we + need to map @start and @end to session. + + Parameters + ---------- + start : Timestamp + end : Timestamp + + Returns + ------- + DatetimeIndex + A DatetimeIndex populated with all of the trading days between + the given start and end. + """ + start_session = self.session_date(start) + end_session = self.session_date(end) + # Increment end_session by one day, beucase .loc[s:e] return all values + # in the DataFrame up to but not including `e`. + # end_session += Timedelta(days=1) + return self.schedule.loc[start_session:end_session] + + def open_and_close(self, dt): + """ + Given a datetime, returns a tuple of timestamps of the + open and close of the exchange session containing the datetime. + + SD: Should we accept an arbitrary datetime, or should we first map it + to and exchange session using session_date. Need to check what the + consumers expect. Here, I assume we need to map it to a session. + + Parameters + ---------- + dt : Timestamp + A dt in a session whose open and close are needed. + + Returns + ------- + (Timestamp, Timestamp) + The open and close for the given dt. + """ + session = self.session_date(dt) + return self._get_open_and_close(session) + + def _get_open_and_close(self, session_date): + """ + Retrieves the open and close for a given session. + + Parameters + ---------- + session_date : Timestamp + The canonicalized session_date whose open and close are needed. + + Returns + ------- + (Timestamp, Timestamp) or (None, None) + The open and close for the given dt, or Nones if the given date is + not a session. + """ + # Return a tuple of nones if the given date is not a session. + if session_date not in self.schedule.index: + return (None, None) + + o_and_c = self.schedule.loc[session_date] + # `market_open` and `market_close` should be timezone aware, but pandas + # 0.16.1 does not appear to support this: + # http://pandas.pydata.org/pandas-docs/stable/whatsnew.html#datetime-with-tz # noqa + return (o_and_c['market_open'].tz_localize('UTC'), + o_and_c['market_close'].tz_localize('UTC')) + + def session_date(self, dt): + """ + Given a datetime, returns the UTC-canonicalized date of the exchange + session in which the time belongs. If the time is not in an exchange + session (while the market is closed), returns the date of the next + exchange session after the time. + + Parameters + ---------- + dt : Timestamp + A timezone-aware Timestamp. + + Returns + ------- + Timestamp + The date of the exchange session in which dt belongs. + """ + # Check if the dt is after the market close + # If so, advance to the next day + if self.is_open_on_day(dt): + _, close = self._get_open_and_close(normalize_date(dt)) + if dt > close: + dt += Timedelta(days=1) + + while not self.is_open_on_day(dt): + dt += Timedelta(days=1) + + return normalize_date(dt) diff --git a/zipline/utils/calendars/exchange_calendar_lse.py b/zipline/utils/calendars/exchange_calendar_lse.py new file mode 100644 index 00000000..f935d8b3 --- /dev/null +++ b/zipline/utils/calendars/exchange_calendar_lse.py @@ -0,0 +1,287 @@ +from datetime import time +from pandas import Timedelta +from pandas.tseries.holiday import( + AbstractHolidayCalendar, + Holiday, + DateOffset, + MO, + weekend_to_monday, + GoodFriday, + EasterMonday, +) +from pytz import timezone + +from zipline.utils.calendars.exchange_calendar import ExchangeCalendar +from zipline.utils.calendars.calendar_helpers import normalize_date + +MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY = range(7) + +# New Year's Day +LSENewYearsDay = Holiday( + "New Year's Day", + month=1, + day=1, + observance=weekend_to_monday, +) +# Early May bank holiday +MayBank = Holiday( + "Early May Bank Holiday", + month=5, + offset=DateOffset(weekday=MO(1)), +) +# Spring bank holiday +SpringBank = Holiday( + "Spring Bank Holiday", + month=5, + day=31, + offset=DateOffset(weekday=MO(-1)), +) +# Summer bank holiday +SummerBank = Holiday( + "Summer Bank Holiday", + month=8, + day=31, + offset=DateOffset(weekday=MO(-1)), +) +# Christmas +Christmas = Holiday( + "Christmas", + month=12, + day=25, +) +# If christmas day is Saturday Monday 27th is a holiday +# If christmas day is sunday the Tuesday 27th is a holiday +WeekendChristmas = Holiday( + "Weekend Christmas", + month=12, + day=27, + days_of_week=(MONDAY, TUESDAY), +) +# Boxing day +BoxingDay = Holiday( + "Boxing Day", + month=12, + day=26, +) +# If boxing day is saturday then Monday 28th is a holiday +# If boxing day is sunday then Tuesday 28th is a holiday +WeekendBoxingDay = Holiday( + "Weekend Boxing Day", + month=12, + day=28, + days_of_week=(MONDAY, TUESDAY), +) + + +class LSEHolidayCalendar(AbstractHolidayCalendar): + """ + Non-trading days for the LSE. + + See NYSEExchangeCalendar for full description. + """ + rules = [ + LSENewYearsDay, + GoodFriday, + EasterMonday, + MayBank, + SpringBank, + SummerBank, + Christmas, + WeekendChristmas, + BoxingDay, + WeekendBoxingDay, + ] + + +class LSEExchangeCalendar(ExchangeCalendar): + """ + Exchange calendar for the London Stock Exchange + + Open Time: 8:00 AM, GMT + Close Time: 4:30 PM, GMT + + Regularly-Observed Holidays: + - New Years Day (observed on first business day on/after) + - Good Friday + - Easter Monday + - Early May Bank Holiday (first Monday in May) + - Spring Bank Holiday (last Monday in May) + - Summer Bank Holiday (last Monday in May) + - Christmas Day + - Dec. 27th (if Christmas is on a weekend) + - Boxing Day + - Dec. 28th (if Boxing Day is on a weekend) + """ + + exchange_name = 'LSE' + native_timezone = timezone('Europe/London') + open_time = time(8, 01) + close_time = time(16, 30) + open_offset = 0 + close_offset = 0 + + holidays_calendar = LSEHolidayCalendar() + special_opens_calendars = () + special_closes_calendars = () + + holidays_adhoc = () + + special_opens_adhoc = () + special_closes_adhoc = () + + @property + def name(self): + """ + The name of this exchange calendar. + E.g.: 'NYSE', 'LSE', 'CME Energy' + """ + return self.exchange_name + + @property + def tz(self): + """ + The native timezone of the exchange. + """ + return self.native_timezone + + def is_open_on_minute(self, dt): + """ + Is the exchange open (accepting orders) at @dt. + + Parameters + ---------- + dt : Timestamp + + Returns + ------- + bool + True if exchange is open at the given dt, otherwise False. + """ + # Retrieve the exchange session relevant for this datetime + session = self.session_date(dt) + # Retrieve the open and close for this exchange session + open, close = self.open_and_close(session) + # Is @dt within the trading hours for this exchange session + return open <= dt and dt <= close + + def is_open_on_day(self, dt): + """ + Is the exchange open (accepting orders) anytime during the calendar day + containing @dt. + + Parameters + ---------- + dt : Timestamp + + Returns + ------- + bool + True if exchange is open at any time during the day containing @dt + """ + dt_normalized = normalize_date(dt) + return dt_normalized in self.schedule.index + + def trading_days(self, start, end): + """ + Calculates all of the exchange sessions between the given + start and end, inclusive. + + SD: Should @start and @end are UTC-canonicalized, as our exchange + sessions are. If not, then it's not clear how this method should behave + if @start and @end are both in the middle of the day. Here, I assume we + need to map @start and @end to session. + + Parameters + ---------- + start : Timestamp + end : Timestamp + + Returns + ------- + DatetimeIndex + A DatetimeIndex populated with all of the trading days between + the given start and end. + """ + start_session = self.session_date(start) + end_session = self.session_date(end) + # Increment end_session by one day, beucase .loc[s:e] return all values + # in the DataFrame up to but not including `e`. + # end_session += Timedelta(days=1) + return self.schedule.loc[start_session:end_session] + + def open_and_close(self, dt): + """ + Given a datetime, returns a tuple of timestamps of the + open and close of the exchange session containing the datetime. + + SD: Should we accept an arbitrary datetime, or should we first map it + to and exchange session using session_date. Need to check what the + consumers expect. Here, I assume we need to map it to a session. + + Parameters + ---------- + dt : Timestamp + A dt in a session whose open and close are needed. + + Returns + ------- + (Timestamp, Timestamp) + The open and close for the given dt. + """ + session = self.session_date(dt) + return self._get_open_and_close(session) + + def _get_open_and_close(self, session_date): + """ + Retrieves the open and close for a given session. + + Parameters + ---------- + session_date : Timestamp + The canonicalized session_date whose open and close are needed. + + Returns + ------- + (Timestamp, Timestamp) or (None, None) + The open and close for the given dt, or Nones if the given date is + not a session. + """ + # Return a tuple of nones if the given date is not a session. + if session_date not in self.schedule.index: + return (None, None) + + o_and_c = self.schedule.loc[session_date] + # `market_open` and `market_close` should be timezone aware, but pandas + # 0.16.1 does not appear to support this: + # http://pandas.pydata.org/pandas-docs/stable/whatsnew.html#datetime-with-tz # noqa + return (o_and_c['market_open'].tz_localize('UTC'), + o_and_c['market_close'].tz_localize('UTC')) + + def session_date(self, dt): + """ + Given a datetime, returns the UTC-canonicalized date of the exchange + session in which the time belongs. If the time is not in an exchange + session (while the market is closed), returns the date of the next + exchange session after the time. + + Parameters + ---------- + dt : Timestamp + A timezone-aware Timestamp. + + Returns + ------- + Timestamp + The date of the exchange session in which dt belongs. + """ + # Check if the dt is after the market close + # If so, advance to the next day + if self.is_open_on_day(dt): + _, close = self._get_open_and_close(normalize_date(dt)) + if dt > close: + dt += Timedelta(days=1) + + while not self.is_open_on_day(dt): + dt += Timedelta(days=1) + + return normalize_date(dt) diff --git a/zipline/utils/calendars/nyse_exchange_calendar.py b/zipline/utils/calendars/exchange_calendar_nyse.py similarity index 99% rename from zipline/utils/calendars/nyse_exchange_calendar.py rename to zipline/utils/calendars/exchange_calendar_nyse.py index 34fd2633..c3913d92 100644 --- a/zipline/utils/calendars/nyse_exchange_calendar.py +++ b/zipline/utils/calendars/exchange_calendar_nyse.py @@ -54,7 +54,7 @@ NYSE_OPEN = time(9, 31) NYSE_CLOSE = time(16) NYSE_STANDARD_EARLY_CLOSE = time(13) # Does the market open or close on a different calendar day, compared to the -# calendar day assigned by the exchang to this session? +# calendar day assigned by the exchange to this session? NYSE_OPEN_OFFSET = 0 NYSE_CLOSE_OFFSET = 0 diff --git a/zipline/utils/calendars/exchange_calendar_tsx.py b/zipline/utils/calendars/exchange_calendar_tsx.py new file mode 100644 index 00000000..5fb571f8 --- /dev/null +++ b/zipline/utils/calendars/exchange_calendar_tsx.py @@ -0,0 +1,291 @@ +from datetime import time +from pandas import Timedelta +from pandas.tseries.holiday import( + AbstractHolidayCalendar, + Holiday, + DateOffset, + MO, + weekend_to_monday, + GoodFriday, +) +from pytz import timezone + +from zipline.utils.calendars.exchange_calendar import ExchangeCalendar +from zipline.utils.calendars.calendar_helpers import normalize_date +from zipline.utils.calendars.exchange_calendar_lse import ( + Christmas, + WeekendChristmas, + BoxingDay, + WeekendBoxingDay, +) + +MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY = range(7) + +# New Year's Day +TSXNewYearsDay = Holiday( + "New Year's Day", + month=1, + day=1, + observance=weekend_to_monday, +) +# Ontario Family Day +FamilyDay = Holiday( + "Family Day", + month=2, + day=1, + offset=DateOffset(weekday=MO(3)), + start_date='2008-01-01', +) +# Victoria Day +VictoriaDay = Holiday( + 'Victoria Day', + month=5, + day=25, + offset=DateOffset(weekday=MO(-1)), +) +# Canada Day +CanadaDay = Holiday( + 'Canada Day', + month=7, + day=1, + observance=weekend_to_monday, +) +# Civic Holiday +CivicHoliday = Holiday( + 'Civic Holiday', + month=8, + day=1, + offset=DateOffset(weekday=MO(1)), +) +# Labor Day +LaborDay = Holiday( + 'Labor Day', + month=9, + day=1, + offset=DateOffset(weekday=MO(1)), +) +# Thanksgiving +Thanksgiving = Holiday( + 'Thanksgiving', + month=10, + day=1, + offset=DateOffset(weekday=MO(2)), +) + + +class TSXHolidayCalendar(AbstractHolidayCalendar): + """ + Non-trading days for the TSX. + + See NYSEExchangeCalendar for full description. + """ + rules = [ + TSXNewYearsDay, + FamilyDay, + GoodFriday, + VictoriaDay, + CanadaDay, + CivicHoliday, + LaborDay, + Thanksgiving, + Christmas, + WeekendChristmas, + BoxingDay, + WeekendBoxingDay, + ] + + +class TSXExchangeCalendar(ExchangeCalendar): + """ + Exchange calendar for the Toronto Stock Exchange + + Open Time: 9:30 AM, EST + Close Time: 4:00 PM, EST + + Regularly-Observed Holidays: + - New Years Day (observed on first business day on/after) + - Family Day (Third Monday in February after 2008) + - Good Friday + - Victoria Day (Monday before May 25th) + - Canada Day (July 1st, observed first business day after) + - Civic Holiday (First Monday in August) + - Labor Day (First Monday in September) + - Thanksgiving (Second Monday in October) + - Christmas Day + - Dec. 27th (if Christmas is on a weekend) + - Boxing Day + - Dec. 28th (if Boxing Day is on a weekend) + """ + + exchange_name = 'TSX' + native_timezone = timezone('Canada/Atlantic') + open_time = time(9, 31) + close_time = time(16) + open_offset = 0 + close_offset = 0 + + holidays_calendar = TSXHolidayCalendar() + special_opens_calendars = () + special_closes_calendars = () + + holidays_adhoc = () + + special_opens_adhoc = () + special_closes_adhoc = () + + @property + def name(self): + """ + The name of this exchange calendar. + E.g.: 'NYSE', 'LSE', 'CME Energy' + """ + return self.exchange_name + + @property + def tz(self): + """ + The native timezone of the exchange. + """ + return self.native_timezone + + def is_open_on_minute(self, dt): + """ + Is the exchange open (accepting orders) at @dt. + + Parameters + ---------- + dt : Timestamp + + Returns + ------- + bool + True if exchange is open at the given dt, otherwise False. + """ + # Retrieve the exchange session relevant for this datetime + session = self.session_date(dt) + # Retrieve the open and close for this exchange session + open, close = self.open_and_close(session) + # Is @dt within the trading hours for this exchange session + return open <= dt and dt <= close + + def is_open_on_day(self, dt): + """ + Is the exchange open (accepting orders) anytime during the calendar day + containing @dt. + + Parameters + ---------- + dt : Timestamp + + Returns + ------- + bool + True if exchange is open at any time during the day containing @dt + """ + dt_normalized = normalize_date(dt) + return dt_normalized in self.schedule.index + + def trading_days(self, start, end): + """ + Calculates all of the exchange sessions between the given + start and end, inclusive. + + SD: Should @start and @end are UTC-canonicalized, as our exchange + sessions are. If not, then it's not clear how this method should behave + if @start and @end are both in the middle of the day. Here, I assume we + need to map @start and @end to session. + + Parameters + ---------- + start : Timestamp + end : Timestamp + + Returns + ------- + DatetimeIndex + A DatetimeIndex populated with all of the trading days between + the given start and end. + """ + start_session = self.session_date(start) + end_session = self.session_date(end) + # Increment end_session by one day, beucase .loc[s:e] return all values + # in the DataFrame up to but not including `e`. + # end_session += Timedelta(days=1) + return self.schedule.loc[start_session:end_session] + + def open_and_close(self, dt): + """ + Given a datetime, returns a tuple of timestamps of the + open and close of the exchange session containing the datetime. + + SD: Should we accept an arbitrary datetime, or should we first map it + to and exchange session using session_date. Need to check what the + consumers expect. Here, I assume we need to map it to a session. + + Parameters + ---------- + dt : Timestamp + A dt in a session whose open and close are needed. + + Returns + ------- + (Timestamp, Timestamp) + The open and close for the given dt. + """ + session = self.session_date(dt) + return self._get_open_and_close(session) + + def _get_open_and_close(self, session_date): + """ + Retrieves the open and close for a given session. + + Parameters + ---------- + session_date : Timestamp + The canonicalized session_date whose open and close are needed. + + Returns + ------- + (Timestamp, Timestamp) or (None, None) + The open and close for the given dt, or Nones if the given date is + not a session. + """ + # Return a tuple of nones if the given date is not a session. + if session_date not in self.schedule.index: + return (None, None) + + o_and_c = self.schedule.loc[session_date] + # `market_open` and `market_close` should be timezone aware, but pandas + # 0.16.1 does not appear to support this: + # http://pandas.pydata.org/pandas-docs/stable/whatsnew.html#datetime-with-tz # noqa + return (o_and_c['market_open'].tz_localize('UTC'), + o_and_c['market_close'].tz_localize('UTC')) + + def session_date(self, dt): + """ + Given a datetime, returns the UTC-canonicalized date of the exchange + session in which the time belongs. If the time is not in an exchange + session (while the market is closed), returns the date of the next + exchange session after the time. + + Parameters + ---------- + dt : Timestamp + A timezone-aware Timestamp. + + Returns + ------- + Timestamp + The date of the exchange session in which dt belongs. + """ + # Check if the dt is after the market close + # If so, advance to the next day + if self.is_open_on_day(dt): + _, close = self._get_open_and_close(normalize_date(dt)) + if dt > close: + dt += Timedelta(days=1) + + while not self.is_open_on_day(dt): + dt += Timedelta(days=1) + + return normalize_date(dt)