Repository: rsheftel/pandas_market_calendars

Issue:
CBOE_Index_Options calendar not reporting holidays correctly with Pandas 2.0.0
1:
```
import pandas_market_calendars as mcal
from holidays import UnitedStates # This is just a shortcut to hand coding a bunch of holidays

cboe = mcal.get_calendar("CBOE_Index_Options")
for date, _ in UnitedStates(years=2023).items():
    print(cboe.schedule(date, date))
    print()
````

1a (output):
```
Empty DataFrame
Columns: [market_open, market_close]
Index: []

                         market_open              market_close
2023-01-02 2023-01-02 14:30:00+00:00 2023-01-02 21:15:00+00:00

                         market_open              market_close
2023-01-16 2023-01-16 14:30:00+00:00 2023-01-16 21:15:00+00:00

                         market_open              market_close
2023-02-20 2023-02-20 14:30:00+00:00 2023-02-20 21:15:00+00:00

                         market_open              market_close
2023-05-29 2023-05-29 13:30:00+00:00 2023-05-29 20:15:00+00:00

                         market_open              market_close
2023-06-19 2023-06-19 13:30:00+00:00 2023-06-19 20:15:00+00:00

                         market_open              market_close
2023-07-04 2023-07-04 13:30:00+00:00 2023-07-04 20:15:00+00:00

                         market_open              market_close
2023-09-04 2023-09-04 13:30:00+00:00 2023-09-04 20:15:00+00:00

                         market_open              market_close
2023-10-09 2023-10-09 13:30:00+00:00 2023-10-09 20:15:00+00:00

Empty DataFrame
Columns: [market_open, market_close]
Index: []

                         market_open              market_close
2023-11-10 2023-11-10 14:30:00+00:00 2023-11-10 21:15:00+00:00

                         market_open              market_close
2023-11-23 2023-11-23 14:30:00+00:00 2023-11-23 21:15:00+00:00

                         market_open              market_close
2023-12-25 2023-12-25 14:30:00+00:00 2023-12-25 21:15:00+00:00
```

2:
```
lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.5 LTS
Release:        20.04
Codename:       focal

python3.9 --version
Python 3.9.16

pip3.9 freeze | grep pandas
pandas==2.0.0
pandas-market-calendars==4.1.4
```

3:
After upgrading to pandas 2.0.0, the CBOE Index Options calendar fails to recognize CBOE's listed holidays (https://www.cboe.com/about/hours). The above output shows what is happening: holidays with open and close times. The expected output is empty dataframes for each holiday.

After downgrading to pandas 1.5.3 all holiday schedules return empty dataframes as expected.


Discussion:

Patch:
diff --git a/.gitignore b/.gitignore
index 3651205..36c8898 100644
--- a/.gitignore
+++ b/.gitignore
@@ -82,7 +82,7 @@ celerybeat-schedule
 .env
 .venv
 env/
-venv/
+venv*/
 env.bak/
 venv.bak/
 
diff --git a/pandas_market_calendars/exchange_calendar_cboe.py b/pandas_market_calendars/exchange_calendar_cboe.py
index 3cb1eb0..fdb3ad7 100644
--- a/pandas_market_calendars/exchange_calendar_cboe.py
+++ b/pandas_market_calendars/exchange_calendar_cboe.py
@@ -18,17 +18,18 @@ def good_friday_unless_christmas_nye_friday(dt):
     Good Friday is a valid trading day if Christmas Day or New Years Day fall
     on a Friday.
     """
+    if isinstance(dt, pd.DatetimeIndex):
+        # Pandas < 2.1.0 will call with an index and fall-back to element by element
+        # Pandas == 2.1.0 will only call element by element
+        raise NotImplementedError()
+
     year = dt.year
-    christmas_weekday = Christmas.observance(
-        pd.Timestamp(year, 12, 25)
-    ).weekday()
-    nyd_weekday = USNewYearsDay.observance(
-        pd.Timestamp(year, 1, 1)
-    ).weekday()
+    christmas_weekday = Christmas.observance(pd.Timestamp(year=year, month=12, day=25)).weekday()
+    nyd_weekday = USNewYearsDay.observance(pd.Timestamp(year=year, month=1, day=1)).weekday()
     if christmas_weekday != 4 and nyd_weekday != 4:
-        return GoodFriday._apply_rule(dt)
+        return GoodFriday.dates(pd.Timestamp(year=year, month=1, day=1), pd.Timestamp(year=year, month=12, day=31))[0]
     else:
-        # compatibility for pandas 0.18.1
+        # Not a holiday so use NaT to ensure it gets removed
         return pd.NaT
 
 
@@ -71,8 +72,7 @@ class CFEExchangeCalendar(MarketCalendar):
             USNewYearsDay,
             USMartinLutherKingJrAfter1998,
             USPresidentsDay,
-            # GoodFridayUnlessChristmasNYEFriday, #TODO: When this is fixed can return to using it
-            GoodFriday,
+            GoodFridayUnlessChristmasNYEFriday,
             USJuneteenthAfter2022,
             USIndependenceDay,
             USMemorialDay,
diff --git a/pandas_market_calendars/holidays_nyse.py b/pandas_market_calendars/holidays_nyse.py
index e02771b..06db5bc 100644
--- a/pandas_market_calendars/holidays_nyse.py
+++ b/pandas_market_calendars/holidays_nyse.py
@@ -1,3 +1,4 @@
+import pandas as pd
 from dateutil.relativedelta import (MO, TH, TU)
 from pandas import (DateOffset, Timestamp, date_range)
 from datetime import  timedelta
@@ -302,9 +303,6 @@ MonTuesThursBeforeIndependenceDay = Holiday(
     start_date=Timestamp("1995-01-01"),
 )
 
-def july_5th_holiday_observance(datetime_index):
-    return datetime_index[datetime_index.year < 2013]
-
 FridayAfterIndependenceDayNYSEpre2013 = Holiday(
     # When July 4th is a Thursday, the next day is a half day prior to 2013.
     # Since 2013 the early close is on Wednesday and Friday is a full day
@@ -312,8 +310,8 @@ FridayAfterIndependenceDayNYSEpre2013 = Holiday(
     month=7,
     day=5,
     days_of_week=(FRIDAY,),
-    observance=july_5th_holiday_observance,
     start_date=Timestamp("1996-01-01"),
+    end_date=Timestamp("2012-12-31"),
 )
 
 WednesdayBeforeIndependenceDayPost2013 = Holiday(
diff --git a/pandas_market_calendars/holidays_us.py b/pandas_market_calendars/holidays_us.py
index 74f6855..00a9e40 100644
--- a/pandas_market_calendars/holidays_us.py
+++ b/pandas_market_calendars/holidays_us.py
@@ -10,10 +10,6 @@ from pandas_market_calendars.market_calendar import (FRIDAY, MONDAY, THURSDAY, T
 # NYSE closed at 2:00 PM on Christmas Eve until 1993.
 
 
-def july_5th_holiday_observance(datetime_index):
-    return datetime_index[datetime_index.year < 2013]
-
-
 def following_tuesday_every_four_years_observance(dt):
     return dt + DateOffset(years=(4 - (dt.year % 4)) % 4, weekday=TU(1))
 
@@ -201,8 +197,8 @@ FridayAfterIndependenceDayPre2013 = Holiday(
     month=7,
     day=5,
     days_of_week=(FRIDAY,),
-    observance=july_5th_holiday_observance,
     start_date=Timestamp("1995-01-01"),
+    end_date=Timestamp("2012-12-31"),
 )
 WednesdayBeforeIndependenceDayPost2013 = Holiday(
     # When July 4th is a Thursday, the next day is a half day prior to 2013.
diff --git a/pandas_market_calendars/market_calendar.py b/pandas_market_calendars/market_calendar.py
index 72cee58..da4e819 100644
--- a/pandas_market_calendars/market_calendar.py
+++ b/pandas_market_calendars/market_calendar.py
@@ -422,6 +422,7 @@ class MarketCalendar(metaclass=MarketCalendarMeta):
         return []
 
     def _convert(self, col):
+        col = col.dropna()  # Python 3.8, pandas 2.0.3 cannot create time deltas from NaT
         try: times = col.str[0]
         except AttributeError: # no tuples, only offset 0
             return (pd.to_timedelta(col.astype("string"), errors="coerce") + col.index


Test patch:
diff --git a/tests/test_cboe_calendars.py b/tests/test_cboe_calendars.py
index 1af08be..f8d4fbb 100644
--- a/tests/test_cboe_calendars.py
+++ b/tests/test_cboe_calendars.py
@@ -43,7 +43,6 @@ def test_2016_holidays():
 def test_good_friday_rule():
     # Good friday is a holiday unless Christmas Day or New Years Day is on a Friday
     for calendar in calendars:
-
         cal = calendar()
         valid_days = cal.valid_days('2015-04-01', '2016-04-01')
         for day in ["2015-04-03", "2016-03-25"]:
diff --git a/tests/test_market_calendar.py b/tests/test_market_calendar.py
index 14c184e..47b63e5 100644
--- a/tests/test_market_calendar.py
+++ b/tests/test_market_calendar.py
@@ -544,10 +544,13 @@ def test_schedule():
     assert_series_equal(results.iloc[-1], expected)
 
     # one day schedule
-    expected = pd.DataFrame({'market_open': pd.Timestamp('2016-12-01 03:13:00+0000', tz='UTC'),
-                             'market_close': pd.Timestamp('2016-12-01 03:49:00+0000', tz='UTC')},
-                            index=pd.DatetimeIndex([pd.Timestamp('2016-12-01')], freq='C'),
-                            columns=['market_open', 'market_close'])
+    expected = pd.DataFrame(
+        {'market_open': pd.Timestamp('2016-12-01 03:13:00+0000', tz='UTC'),
+         'market_close': pd.Timestamp('2016-12-01 03:49:00+0000', tz='UTC')},
+        index=pd.DatetimeIndex([pd.Timestamp('2016-12-01')], freq='C'),
+        columns=['market_open', 'market_close'],
+        dtype="datetime64[ns, UTC]"
+    )
     actual = cal.schedule('2016-12-01', '2016-12-01')
     if pd.__version__ < '1.1.0':
         assert_frame_equal(actual, expected)
@@ -557,12 +560,15 @@ def test_schedule():
     # start date after end date
     with pytest.raises(ValueError):
         cal.schedule('2016-02-02', '2016-01-01')
-    
+
     # using a different time zone
-    expected = pd.DataFrame({'market_open': pd.Timestamp('2016-11-30 22:13:00-05:00', tz='US/Eastern'),
-                             'market_close': pd.Timestamp('2016-11-30 22:49:00-05:00', tz='US/Eastern')},
-                            index=pd.DatetimeIndex([pd.Timestamp('2016-12-01')]),
-                            columns=['market_open', 'market_close'])
+    expected = pd.DataFrame(
+        {'market_open': pd.Timestamp('2016-11-30 22:13:00-05:00', tz='US/Eastern'),
+         'market_close': pd.Timestamp('2016-11-30 22:49:00-05:00', tz='US/Eastern')},
+        index=pd.DatetimeIndex([pd.Timestamp('2016-12-01')]),
+        columns=['market_open', 'market_close'],
+        dtype="datetime64[ns, US/Eastern]"
+    )
 
     actual = cal.schedule('2016-12-01', '2016-12-01', tz='US/Eastern')
     if pd.__version__ < '1.1.0':
@@ -704,12 +710,15 @@ def test_schedule_w_breaks():
     assert_series_equal(results.iloc[-1], expected)
 
     # using a different time zone
-    expected = pd.DataFrame({'market_open': pd.Timestamp('2016-12-28 09:30:00-05:00', tz='America/New_York'),
-                             'market_close': pd.Timestamp('2016-12-28 12:00:00-05:00', tz='America/New_York'),
-                             'break_start': pd.Timestamp('2016-12-28 10:00:00-05:00', tz='America/New_York'),
-                             'break_end': pd.Timestamp('2016-12-28 11:00:00-05:00', tz='America/New_York')},
-                            index=pd.DatetimeIndex([pd.Timestamp('2016-12-28')]),
-                            columns=['market_open', 'break_start', 'break_end', 'market_close'])
+    expected = pd.DataFrame(
+        {'market_open': pd.Timestamp('2016-12-28 09:30:00-05:00', tz='America/New_York'),
+         'market_close': pd.Timestamp('2016-12-28 12:00:00-05:00', tz='America/New_York'),
+         'break_start': pd.Timestamp('2016-12-28 10:00:00-05:00', tz='America/New_York'),
+         'break_end': pd.Timestamp('2016-12-28 11:00:00-05:00', tz='America/New_York')},
+        index=pd.DatetimeIndex([pd.Timestamp('2016-12-28')]),
+        columns=['market_open', 'break_start', 'break_end', 'market_close'],
+        dtype="datetime64[ns, America/New_York]"
+    )
 
     actual = cal.schedule('2016-12-28', '2016-12-28', tz='America/New_York')
     if pd.__version__ < '1.1.0':
diff --git a/tests/test_nyse_calendar.py b/tests/test_nyse_calendar.py
index e215994..81c66d6 100644
--- a/tests/test_nyse_calendar.py
+++ b/tests/test_nyse_calendar.py
@@ -347,7 +347,7 @@ def test_all_full_day_holidays_since_1928(request):
     assert_index_equal(expected, actual)
 
     # using the holidays method
-    actual = pd.DatetimeIndex(nyse.holidays().holidays).unique().astype('datetime64[ns]')
+    actual = pd.DatetimeIndex(nyse.holidays().holidays, dtype='datetime64[ns]').unique()
     slice_locs = actual.slice_locs(expected[0], expected[-1])
     actual = actual[slice_locs[0]:slice_locs[1]]
     assert_index_equal(expected, actual)