ENH: Adds BMF, LSE, and TSX exchange calendars

This commit is contained in:
jfkirk
2016-05-24 14:26:44 -04:00
committed by Jean Bredeche
parent 219f20989f
commit da99cd6192
6 changed files with 957 additions and 3 deletions
+1 -1
View File
@@ -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):
+1 -1
View File
@@ -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)
@@ -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)
@@ -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)
@@ -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
@@ -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)