Repository: rsheftel/pandas_market_calendars

Issue:
IEX Integration
Any plans / blocks with adding integration for [IEX's](https://exchange.iex.io/) trading calendar? Haven't ever seen them deviate from NYSE but would be nice to have. 

If there are no objections I will submit a PR for this integration. 

Comments:

Patch:
diff --git a/pandas_market_calendars/calendar_registry.py b/pandas_market_calendars/calendar_registry.py
index 5ea2e60..21ea581 100644
--- a/pandas_market_calendars/calendar_registry.py
+++ b/pandas_market_calendars/calendar_registry.py
@@ -9,6 +9,7 @@ from .exchange_calendar_cme import \
 from .exchange_calendar_eurex import EUREXExchangeCalendar
 from .exchange_calendar_hkex import HKEXExchangeCalendar
 from .exchange_calendar_ice import ICEExchangeCalendar
+from .exchange_calendar_iex import IEXExchangeCalendar
 from .exchange_calendar_jpx import JPXExchangeCalendar
 from .exchange_calendar_lse import LSEExchangeCalendar
 from .exchange_calendar_nyse import NYSEExchangeCalendar
diff --git a/pandas_market_calendars/exchange_calendar_iex.py b/pandas_market_calendars/exchange_calendar_iex.py
new file mode 100644
index 0000000..b1a12ca
--- /dev/null
+++ b/pandas_market_calendars/exchange_calendar_iex.py
@@ -0,0 +1,99 @@
+from datetime import time
+from itertools import chain
+from .exchange_calendar_nyse import NYSEExchangeCalendar 
+from pandas.tseries.holiday import AbstractHolidayCalendar
+from pytz import timezone 
+
+from pandas_market_calendars.holidays_nyse import (
+    USPresidentsDay,
+    GoodFriday,
+    USMemorialDay,
+    USJuneteenthAfter2022,
+    USIndependenceDay,
+    USThanksgivingDay,
+    ChristmasNYSE,
+    USMartinLutherKingJrAfter1998,
+
+    #Ad-Hoc
+    DayAfterThanksgiving1pmEarlyCloseInOrAfter1993,
+    DaysBeforeIndependenceDay1pmEarlyCloseAdhoc,
+    ChristmasEvesAdhoc,
+)
+
+class IEXExchangeCalendar(NYSEExchangeCalendar):
+    """
+    Exchange calendar for the Investor's Exchange (IEX).
+
+    IEX is an equities exchange, based in New York City, US 
+    that integrates proprietary technology to facilitate fair
+    play between HFT firms and retail investors. 
+
+    Most of this class inherits from NYSEExchangeCalendar since 
+    the holdiays are the same. The only variation is (1) IEX began 
+    operation in 2013, and (2) IEX has different hours of operation
+
+    References: 
+    - https://exchange.iex.io/
+    - https://iexexchange.io/resources/trading/trading-hours-holidays/index.html
+    """
+
+    regular_market_times = {
+        "pre": (('2013-03-25', time(8)),),
+        "market_open": ((None, time(9, 30)),),
+        "market_close":((None, time(16)),),
+        "post": ((None, time(17)),)
+    }
+
+    aliases = ['IEX', 'Investors_Exchange']
+
+    @property
+    def name(self):
+        return "IEX"
+    
+    @property
+    def weekmask(self):
+        return "Mon Tue Wed Thu Fri"
+    
+    @property
+    def regular_holidays(self):
+        return AbstractHolidayCalendar(rules=[
+            USPresidentsDay,
+            GoodFriday,
+            USMemorialDay,
+            USJuneteenthAfter2022,
+            USIndependenceDay,
+            USThanksgivingDay,
+            ChristmasNYSE,
+            USMartinLutherKingJrAfter1998
+        ])
+    
+    @property
+    def adhoc_holidays(self):
+        return list(chain(
+            ChristmasEvesAdhoc,
+        ))
+
+    @property
+    def special_closes(self):
+        return [
+            (time(hour=13, tzinfo=timezone('America/New_York')), AbstractHolidayCalendar(rules=[
+                DayAfterThanksgiving1pmEarlyCloseInOrAfter1993,
+            ]))
+        ]
+
+    """Override NYSE calendar special cases"""
+
+    @property
+    def special_closes_adhoc(self):
+        return [
+            (time(13, tzinfo=timezone('America/New_York')),
+                DaysBeforeIndependenceDay1pmEarlyCloseAdhoc) 
+        ]
+
+    @property
+    def special_opens(self):
+        return []
+
+    def valid_days(self, start_date, end_date, tz='UTC'):
+        trading_days = super().valid_days(start_date, end_date, tz=tz) #all NYSE valid days
+        return trading_days[~(trading_days <= '2013-08-25')]


Test patch:
diff --git a/tests/test_iex_calendar.py b/tests/test_iex_calendar.py
new file mode 100644
index 0000000..c6d325f
--- /dev/null
+++ b/tests/test_iex_calendar.py
@@ -0,0 +1,35 @@
+import pandas as pd 
+import numpy as np 
+from datetime import time 
+from pytz import timezone 
+from pandas_market_calendars.exchange_calendar_iex import IEXExchangeCalendar
+from pandas_market_calendars.class_registry import _ProtectedDict 
+
+iex = IEXExchangeCalendar()
+
+def test_time_zone():
+    assert iex.tz == timezone("America/New_York")
+    assert iex.name == "IEX"
+
+
+def test_open_close():
+    assert iex.open_time == time(9, 30, tzinfo=timezone('America/New_York'))
+    assert iex.close_time == time(16, tzinfo=timezone('America/New_York'))
+
+
+def test_calendar_utility():
+    assert len(iex.holidays().holidays) > 0
+    assert isinstance(iex.regular_market_times, _ProtectedDict)
+    
+    valid_days = iex.valid_days(start_date='2016-12-20', end_date='2017-01-10')
+    assert isinstance(valid_days, pd.DatetimeIndex)
+    assert not valid_days.empty
+
+    schedule = iex.schedule(start_date='2015-07-01', end_date='2017-07-10', start="pre", end="post")
+    assert isinstance(schedule, pd.DataFrame)
+    assert not schedule.empty
+
+
+def test_trading_days_before_operation():
+    trading_days = iex.valid_days(start_date="2000-01-01", end_date="2022-02-23")
+    assert np.array([~(trading_days <= '2013-08-25')]).any()