pandas-market-calendars 5.0.0__tar.gz → 5.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. {pandas_market_calendars-5.0.0/pandas_market_calendars.egg-info → pandas_market_calendars-5.1.0}/PKG-INFO +1 -1
  2. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/pandas_market_calendars/calendar_utils.py +8 -4
  3. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/pandas_market_calendars/calendars/sifma.py +42 -15
  4. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/pandas_market_calendars/holidays/sifma.py +31 -31
  5. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0/pandas_market_calendars.egg-info}/PKG-INFO +1 -1
  6. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/pyproject.toml +1 -1
  7. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/tests/test_sifma_calendars.py +126 -19
  8. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/tests/test_utils.py +27 -4
  9. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/LICENSE +0 -0
  10. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/NOTICE +0 -0
  11. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/README.rst +0 -0
  12. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/pandas_market_calendars/__init__.py +0 -0
  13. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/pandas_market_calendars/calendar_registry.py +0 -0
  14. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/pandas_market_calendars/calendars/__init__.py +0 -0
  15. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/pandas_market_calendars/calendars/asx.py +0 -0
  16. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/pandas_market_calendars/calendars/bmf.py +0 -0
  17. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/pandas_market_calendars/calendars/bse.py +0 -0
  18. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/pandas_market_calendars/calendars/cboe.py +0 -0
  19. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/pandas_market_calendars/calendars/cme.py +0 -0
  20. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/pandas_market_calendars/calendars/cme_globex_agriculture.py +0 -0
  21. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/pandas_market_calendars/calendars/cme_globex_base.py +0 -0
  22. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/pandas_market_calendars/calendars/cme_globex_crypto.py +0 -0
  23. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/pandas_market_calendars/calendars/cme_globex_energy_and_metals.py +0 -0
  24. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/pandas_market_calendars/calendars/cme_globex_equities.py +0 -0
  25. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/pandas_market_calendars/calendars/cme_globex_fixed_income.py +0 -0
  26. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/pandas_market_calendars/calendars/cme_globex_fx.py +0 -0
  27. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/pandas_market_calendars/calendars/eurex.py +0 -0
  28. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/pandas_market_calendars/calendars/eurex_fixed_income.py +0 -0
  29. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/pandas_market_calendars/calendars/hkex.py +0 -0
  30. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/pandas_market_calendars/calendars/ice.py +0 -0
  31. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/pandas_market_calendars/calendars/iex.py +0 -0
  32. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/pandas_market_calendars/calendars/jpx.py +0 -0
  33. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/pandas_market_calendars/calendars/lse.py +0 -0
  34. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/pandas_market_calendars/calendars/mirror.py +0 -0
  35. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/pandas_market_calendars/calendars/nyse.py +0 -0
  36. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/pandas_market_calendars/calendars/ose.py +0 -0
  37. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/pandas_market_calendars/calendars/six.py +0 -0
  38. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/pandas_market_calendars/calendars/sse.py +0 -0
  39. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/pandas_market_calendars/calendars/tase.py +0 -0
  40. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/pandas_market_calendars/calendars/tsx.py +0 -0
  41. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/pandas_market_calendars/class_registry.py +0 -0
  42. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/pandas_market_calendars/holidays/__init__.py +0 -0
  43. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/pandas_market_calendars/holidays/cme.py +0 -0
  44. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/pandas_market_calendars/holidays/cme_globex.py +0 -0
  45. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/pandas_market_calendars/holidays/cn.py +0 -0
  46. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/pandas_market_calendars/holidays/jp.py +0 -0
  47. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/pandas_market_calendars/holidays/jpx_equinox.py +0 -0
  48. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/pandas_market_calendars/holidays/nyse.py +0 -0
  49. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/pandas_market_calendars/holidays/oz.py +0 -0
  50. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/pandas_market_calendars/holidays/uk.py +0 -0
  51. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/pandas_market_calendars/holidays/us.py +0 -0
  52. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/pandas_market_calendars/market_calendar.py +0 -0
  53. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/pandas_market_calendars.egg-info/SOURCES.txt +0 -0
  54. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/pandas_market_calendars.egg-info/dependency_links.txt +0 -0
  55. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/pandas_market_calendars.egg-info/requires.txt +0 -0
  56. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/pandas_market_calendars.egg-info/top_level.txt +0 -0
  57. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/setup.cfg +0 -0
  58. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/tests/test_24_7_calendar.py +0 -0
  59. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/tests/test_XNYS_calendar.py +0 -0
  60. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/tests/test_asx_calendar.py +0 -0
  61. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/tests/test_bmf_calendar.py +0 -0
  62. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/tests/test_bse_calendar.py +0 -0
  63. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/tests/test_cboe_calendars.py +0 -0
  64. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/tests/test_class_registry.py +0 -0
  65. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/tests/test_cme_agriculture_calendar.py +0 -0
  66. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/tests/test_cme_bond_calendar.py +0 -0
  67. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/tests/test_cme_equity_calendar.py +0 -0
  68. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/tests/test_date_range.py +0 -0
  69. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/tests/test_eurex_calendar.py +0 -0
  70. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/tests/test_eurex_fixed_income_calendar.py +0 -0
  71. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/tests/test_exchange_calendar_cme_globex_crypto.py +0 -0
  72. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/tests/test_exchange_calendar_cme_globex_energy_and_metals.py +0 -0
  73. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/tests/test_exchange_calendar_cme_globex_equities.py +0 -0
  74. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/tests/test_exchange_calendar_cme_globex_fixed_income.py +0 -0
  75. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/tests/test_exchange_calendar_cme_globex_fx.py +0 -0
  76. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/tests/test_exchange_calendar_cme_globex_grains.py +0 -0
  77. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/tests/test_hkex_calendar.py +0 -0
  78. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/tests/test_ice_calendar.py +0 -0
  79. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/tests/test_iex_calendar.py +0 -0
  80. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/tests/test_jpx_calendar.py +0 -0
  81. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/tests/test_lse_calendar.py +0 -0
  82. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/tests/test_market_calendar.py +0 -0
  83. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/tests/test_nyse_calendar.py +0 -0
  84. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/tests/test_nyse_calendar_early_years.py +0 -0
  85. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/tests/test_ose_calendar.py +0 -0
  86. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/tests/test_six_calendar.py +0 -0
  87. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/tests/test_sse_calendar.py +0 -0
  88. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/tests/test_tsx_calendar.py +0 -0
  89. {pandas_market_calendars-5.0.0 → pandas_market_calendars-5.1.0}/tests/test_xtae_calendar.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pandas_market_calendars
3
- Version: 5.0.0
3
+ Version: 5.1.0
4
4
  Summary: Market and exchange trading calendars for pandas
5
5
  Author-email: Ryan Sheftel <rsheftel@alumni.upenn.edu>
6
6
  License: MIT
@@ -106,11 +106,15 @@ def mark_session(
106
106
  f"Schedule ends at: {schedule.iloc[-1, -1]}"
107
107
  )
108
108
 
109
+ lte_end = schedule.index <= end.normalize().tz_localize(None)
110
+ gte_start = schedule.index >= start.normalize().tz_localize(None)
111
+
112
+ # Shift both by 1 to keep an extra row on either end if available. Needed in some edge cases.
113
+ gte_start = np.append(gte_start, True)[1:] # Shifts gte_start by one to the left.
114
+ lte_end = np.insert(lte_end, 0, True)[:-1] # Shifts lte_end by one to the right.
115
+
109
116
  # Trim the schedule to match the timeframe covered by the given timeseries
110
- schedule = schedule[
111
- (schedule.index >= start.normalize().tz_localize(None))
112
- & (schedule.index <= end.normalize().tz_localize(None))
113
- ]
117
+ schedule = schedule[gte_start & lte_end]
114
118
 
115
119
  backfilled_map = DEFAULT_LABEL_MAP | label_map
116
120
  mapped_labels = [backfilled_map[label] for label in session_labels]
@@ -1,5 +1,7 @@
1
1
  from datetime import time
2
+ import functools
2
3
 
4
+ import pandas as pd
3
5
  from pandas.tseries.holiday import AbstractHolidayCalendar
4
6
  from zoneinfo import ZoneInfo
5
7
  from itertools import chain
@@ -26,11 +28,13 @@ from pandas_market_calendars.holidays.sifma import (
26
28
  USNewYearsEve2pmEarlyClose,
27
29
  MartinLutherKingJr,
28
30
  USPresidentsDay,
31
+ # --- Good Friday Rules --- #
32
+ is_first_friday,
29
33
  GoodFridayThru2020,
30
34
  DayBeforeGoodFriday2pmEarlyCloseThru2020,
31
- GoodFridayAdHoc,
32
- GoodFriday2pmEarlyCloseAdHoc,
33
- DayBeforeGoodFriday2pmEarlyCloseAdHoc,
35
+ GoodFridayPotentialPost2020, # Potential dates, filtered later
36
+ DayBeforeGoodFridayPotentialPost2020, # Potential dates, filtered later
37
+ # --- End Good Friday Rules --- #
34
38
  DayBeforeUSMemorialDay2pmEarlyClose,
35
39
  USMemorialDay,
36
40
  USJuneteenthAfter2022,
@@ -39,7 +43,6 @@ from pandas_market_calendars.holidays.sifma import (
39
43
  ThursdayBeforeUSIndependenceDay2pmEarlyClose,
40
44
  USLaborDay,
41
45
  USColumbusDay,
42
- USVeteransDay2022,
43
46
  USVeteransDay,
44
47
  USThanksgivingDay,
45
48
  DayAfterThanksgiving2pmEarlyClose,
@@ -118,6 +121,32 @@ class SIFMAUSExchangeCalendar(MarketCalendar):
118
121
  def tz(self):
119
122
  return ZoneInfo("America/New_York")
120
123
 
124
+ # Helper method to calculate and cache dynamic dates
125
+ @functools.lru_cache()
126
+ def _get_dynamic_gf_rules(self):
127
+ # Calculate rules for a wide fixed range to avoid arbitrary cutoffs
128
+ # while preventing infinite generation. 1970-2100 is a reasonable range.
129
+ calc_start = pd.Timestamp("1970-01-01")
130
+ calc_end = pd.Timestamp("2100-12-31")
131
+
132
+ # Filter potential dates based on the start_date of the underlying Holiday rules
133
+ gf_rule_start = GoodFridayPotentialPost2020.start_date
134
+ thurs_rule_start = DayBeforeGoodFridayPotentialPost2020.start_date
135
+
136
+ # Ensure calculation range respects the rule start dates
137
+ effective_gf_start = max(calc_start, gf_rule_start) if gf_rule_start else calc_start
138
+ effective_thurs_start = max(calc_start, thurs_rule_start) if thurs_rule_start else calc_start
139
+
140
+ potential_gf_dates = GoodFridayPotentialPost2020.dates(effective_gf_start, calc_end)
141
+ gf_full_holidays = [d for d in potential_gf_dates if not is_first_friday(d)]
142
+ gf_12pm_early_closes = [d for d in potential_gf_dates if is_first_friday(d)]
143
+
144
+ potential_thurs_dates = DayBeforeGoodFridayPotentialPost2020.dates(effective_thurs_start, calc_end)
145
+ thurs_before_gf_2pm_early_closes = [
146
+ thurs for thurs in potential_thurs_dates if not is_first_friday(thurs + pd.Timedelta(days=1))
147
+ ]
148
+ return gf_full_holidays, gf_12pm_early_closes, thurs_before_gf_2pm_early_closes
149
+
121
150
  @property
122
151
  def regular_holidays(self):
123
152
  return AbstractHolidayCalendar(
@@ -131,7 +160,6 @@ class SIFMAUSExchangeCalendar(MarketCalendar):
131
160
  USIndependenceDay,
132
161
  USLaborDay,
133
162
  USColumbusDay,
134
- USVeteransDay2022,
135
163
  USVeteransDay,
136
164
  USThanksgivingDay,
137
165
  Christmas,
@@ -140,11 +168,8 @@ class SIFMAUSExchangeCalendar(MarketCalendar):
140
168
 
141
169
  @property
142
170
  def adhoc_holidays(self):
143
- return list(
144
- chain(
145
- GoodFridayAdHoc,
146
- )
147
- )
171
+ gf_full_holidays, _, _ = self._get_dynamic_gf_rules()
172
+ return gf_full_holidays
148
173
 
149
174
  @property
150
175
  def special_closes(self):
@@ -168,11 +193,15 @@ class SIFMAUSExchangeCalendar(MarketCalendar):
168
193
 
169
194
  @property
170
195
  def special_closes_adhoc(self):
196
+ _, gf_12pm_early_closes, thurs_before_gf_2pm_early_closes = self._get_dynamic_gf_rules()
171
197
  return [
172
198
  (
173
- time(14, tzinfo=ZoneInfo("America/New_York")),
174
- GoodFriday2pmEarlyCloseAdHoc
175
- + DayBeforeGoodFriday2pmEarlyCloseAdHoc, # list
199
+ time(12), # SIFMA rule specifies 12:00 PM ET
200
+ gf_12pm_early_closes,
201
+ ),
202
+ (
203
+ time(14), # SIFMA rule specifies 2:00 PM ET
204
+ thurs_before_gf_2pm_early_closes,
176
205
  ),
177
206
  ]
178
207
 
@@ -227,7 +256,6 @@ class SIFMAUKExchangeCalendar(MarketCalendar):
227
256
  UKSummerBank,
228
257
  USLaborDay,
229
258
  USColumbusDay,
230
- USVeteransDay2022,
231
259
  USVeteransDay,
232
260
  USThanksgivingDay,
233
261
  UKChristmas,
@@ -331,7 +359,6 @@ class SIFMAJPExchangeCalendar(MarketCalendar):
331
359
  JapanSportsDay2020,
332
360
  JapanHealthAndSportsDay2000To2019,
333
361
  JapanCultureDay,
334
- USVeteransDay2022,
335
362
  USVeteransDay,
336
363
  JapanLaborThanksgivingDay,
337
364
  USThanksgivingDay,
@@ -89,27 +89,31 @@ USPresidentsDay = Holiday(
89
89
  ############################################################
90
90
  # Good Friday
91
91
  ############################################################
92
+
93
+
94
+ def is_first_friday(dt):
95
+ """Check if date is the first Friday of the month"""
96
+ # The first Friday of any month must occur on or before the 7th.
97
+ # This check is sufficient regardless of whether Good Friday is in March or April.
98
+ return dt.weekday() == FRIDAY and dt.day <= 7
99
+
100
+
92
101
  GoodFridayThru2020 = Holiday(
93
- "Good Friday 1908+",
102
+ "Good Friday Thru 2020",
94
103
  end_date=Timestamp("2020-12-31"),
95
104
  month=1,
96
105
  day=1,
97
106
  offset=[Easter(), Day(-2)],
98
107
  )
99
108
 
100
- # 2021 is early close.
101
- # 2022 is a full holiday.
102
- # 2023 is early close.
103
- # 2024 is a full holiday
104
- GoodFridayAdHoc = [
105
- Timestamp("2022-04-15", tz="UTC"),
106
- Timestamp("2024-03-29", tz="UTC"),
107
- ]
108
-
109
- GoodFriday2pmEarlyCloseAdHoc = [
110
- Timestamp("2021-04-02", tz="UTC"),
111
- Timestamp("2023-04-07", tz="UTC"),
112
- ]
109
+ # Generate potential Good Friday dates post 2020 (will be filtered in calendar class)
110
+ GoodFridayPotentialPost2020 = Holiday(
111
+ "Good Friday Potential Post 2020",
112
+ start_date=Timestamp("2021-01-01"),
113
+ month=1,
114
+ day=1,
115
+ offset=[Easter(), Day(-2)],
116
+ )
113
117
 
114
118
  DayBeforeGoodFriday2pmEarlyCloseThru2020 = Holiday(
115
119
  "Day Before Good Friday Thru 2020",
@@ -119,10 +123,14 @@ DayBeforeGoodFriday2pmEarlyCloseThru2020 = Holiday(
119
123
  offset=[Easter(), Day(-3)],
120
124
  )
121
125
 
122
- DayBeforeGoodFriday2pmEarlyCloseAdHoc = [
123
- Timestamp("2022-04-14", tz="UTC"),
124
- Timestamp("2024-03-28", tz="UTC"),
125
- ]
126
+ # Generate potential Thursday before Good Friday dates post 2020 (will be filtered in calendar class)
127
+ DayBeforeGoodFridayPotentialPost2020 = Holiday(
128
+ "Day Before Good Friday Potential Post 2020",
129
+ start_date=Timestamp("2021-01-01"),
130
+ month=1,
131
+ day=1,
132
+ offset=[Easter(), Day(-3)],
133
+ )
126
134
 
127
135
  ##################################################
128
136
  # US Memorial Day (Decoration Day) May 30
@@ -204,20 +212,14 @@ USColumbusDay = Holiday(
204
212
  # When falls on Saturday, no holiday is observed.
205
213
  # When falls on Sunday, the Monday following is a holiday.
206
214
  ##########################################################
207
- USVeteransDay2022 = Holiday(
208
- "Veterans Day Prior to 2023",
209
- month=11,
210
- day=11,
211
- end_date=Timestamp("2022-12-31"),
212
- days_of_week=(MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY),
213
- observance=sunday_to_monday,
214
- )
215
-
216
215
  USVeteransDay = Holiday(
217
216
  "Veterans Day",
218
217
  month=11,
219
218
  day=11,
220
- start_date=Timestamp("2023-12-31"),
219
+ # SIFMA guidance for observing only Mon-Fri or Sunday->Monday
220
+ # appears consistent for many years. This rule doesn't specify
221
+ # a start_date, letting it apply further back if needed by other logic,
222
+ # while effectively covering 2023+ due to the days_of_week filter.
221
223
  days_of_week=(MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY),
222
224
  observance=sunday_to_monday,
223
225
  )
@@ -225,9 +227,7 @@ USVeteransDay = Holiday(
225
227
  ################################################
226
228
  # US Thanksgiving Nov 30
227
229
  ################################################
228
- USThanksgivingDay = Holiday(
229
- "Thanksgiving", month=11, day=1, offset=DateOffset(weekday=TH(4))
230
- )
230
+ USThanksgivingDay = Holiday("Thanksgiving", month=11, day=1, offset=DateOffset(weekday=TH(4)))
231
231
 
232
232
  DayAfterThanksgiving2pmEarlyClose = Holiday(
233
233
  "Black Friday",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pandas_market_calendars
3
- Version: 5.0.0
3
+ Version: 5.1.0
4
4
  Summary: Market and exchange trading calendars for pandas
5
5
  Author-email: Ryan Sheftel <rsheftel@alumni.upenn.edu>
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pandas_market_calendars"
3
- version = "5.0.0"
3
+ version = "5.1.0"
4
4
  authors = [
5
5
  { name = "Ryan Sheftel", email = "rsheftel@alumni.upenn.edu" },
6
6
  ]
@@ -93,6 +93,40 @@ def test_us_weekmask():
93
93
  assert sifma_us.weekmask == "Mon Tue Wed Thu Fri"
94
94
 
95
95
 
96
+ def test_us_2025():
97
+ start = "2025-01-01"
98
+ end = "2025-12-31"
99
+ holidays = [
100
+ pd.Timestamp("2025-01-01", tz="UTC"), # New Year's Day
101
+ pd.Timestamp("2025-01-20", tz="UTC"), # MLK
102
+ pd.Timestamp("2025-02-17", tz="UTC"), # Presidents Day
103
+ pd.Timestamp("2025-04-18", tz="UTC"), # Good Friday (NOT first Friday)
104
+ pd.Timestamp("2025-05-26", tz="UTC"), # Memorial Day
105
+ pd.Timestamp("2025-06-19", tz="UTC"), # Juneteenth
106
+ pd.Timestamp("2025-07-04", tz="UTC"), # Independence Day
107
+ pd.Timestamp("2025-09-01", tz="UTC"), # Labor Day
108
+ pd.Timestamp("2025-10-13", tz="UTC"), # Columbus Day
109
+ pd.Timestamp("2025-11-11", tz="UTC"), # Veterans Day
110
+ pd.Timestamp("2025-11-27", tz="UTC"), # Thanksgiving
111
+ pd.Timestamp("2025-12-25", tz="UTC"), # Christmas
112
+ ]
113
+ _test_holidays(sifma_us, holidays, start, end)
114
+ _test_no_special_opens(sifma_us, start, end)
115
+
116
+ # early closes we expect:
117
+ early_closes = [
118
+ pd.Timestamp(
119
+ "2025-04-17 2:00PM", tz="America/New_York"
120
+ ), # Day before Good Friday (2pm because GF is full holiday)
121
+ pd.Timestamp("2025-05-23 2:00PM", tz="America/New_York"), # Day before Memorial Day
122
+ pd.Timestamp("2025-07-03 2:00PM", tz="America/New_York"), # Day before Independence Day
123
+ pd.Timestamp("2025-11-28 2:00PM", tz="America/New_York"), # Day after Thanksgiving
124
+ pd.Timestamp("2025-12-24 2:00PM", tz="America/New_York"), # Day before Christmas
125
+ pd.Timestamp("2025-12-31 2:00PM", tz="America/New_York"), # New Year's Eve
126
+ ]
127
+ _test_has_early_closes(sifma_us, early_closes, start, end)
128
+
129
+
96
130
  def test_us_2024():
97
131
  start = "2024-01-01"
98
132
  end = "2024-12-31"
@@ -100,7 +134,7 @@ def test_us_2024():
100
134
  pd.Timestamp("2024-01-01", tz="UTC"), # New Year's Day
101
135
  pd.Timestamp("2024-01-15", tz="UTC"), # MLK
102
136
  pd.Timestamp("2024-02-19", tz="UTC"), # Presidents Day
103
- pd.Timestamp("2024-03-29", tz="UTC"), # Good Friday
137
+ pd.Timestamp("2024-03-29", tz="UTC"), # Good Friday (NOT first Friday)
104
138
  pd.Timestamp("2024-05-27", tz="UTC"), # Memorial Day
105
139
  pd.Timestamp("2024-06-19", tz="UTC"), # Juneteenth
106
140
  pd.Timestamp("2024-07-04", tz="UTC"), # Independence Day
@@ -115,19 +149,13 @@ def test_us_2024():
115
149
 
116
150
  # early closes we expect:
117
151
  early_closes = [
118
- pd.Timestamp("2024-03-28 2:00PM", tz="America/New_York"), # Good Friday
119
- pd.Timestamp(
120
- "2024-05-24 2:00PM", tz="America/New_York"
121
- ), # Day before Memorial Day
122
- pd.Timestamp(
123
- "2024-07-03 2:00PM", tz="America/New_York"
124
- ), # Day before Independence Day
125
- pd.Timestamp(
126
- "2024-11-29 2:00PM", tz="America/New_York"
127
- ), # Day after Thanksgiving
128
152
  pd.Timestamp(
129
- "2024-12-24 2:00PM", tz="America/New_York"
130
- ), # Day before Christmas
153
+ "2024-03-28 2:00PM", tz="America/New_York"
154
+ ), # Day before Good Friday (2pm because GF is full holiday)
155
+ pd.Timestamp("2024-05-24 2:00PM", tz="America/New_York"), # Day before Memorial Day
156
+ pd.Timestamp("2024-07-03 2:00PM", tz="America/New_York"), # Day before Independence Day
157
+ pd.Timestamp("2024-11-29 2:00PM", tz="America/New_York"), # Day after Thanksgiving
158
+ pd.Timestamp("2024-12-24 2:00PM", tz="America/New_York"), # Day before Christmas
131
159
  pd.Timestamp("2024-12-31 2:00PM", tz="America/New_York"), # New Year's Eve
132
160
  ]
133
161
  _test_has_early_closes(sifma_us, early_closes, start, end)
@@ -136,10 +164,12 @@ def test_us_2024():
136
164
  def test_us_2023():
137
165
  start = "2023-01-01"
138
166
  end = "2023-12-31"
167
+ # Note: Good Friday 2023-04-07 IS the first Friday -> 12pm early close
139
168
  holidays = [
140
169
  pd.Timestamp("2023-01-02", tz="UTC"), # New Year's Day
141
170
  pd.Timestamp("2023-01-16", tz="UTC"), # MLK
142
171
  pd.Timestamp("2023-02-20", tz="UTC"), # Presidents Day
172
+ # Good Friday is NOT a full holiday
143
173
  pd.Timestamp("2023-05-29", tz="UTC"), # Memorial Day
144
174
  pd.Timestamp("2023-06-19", tz="UTC"), # Juneteenth
145
175
  pd.Timestamp("2023-07-04", tz="UTC"), # Independence Day
@@ -153,7 +183,8 @@ def test_us_2023():
153
183
 
154
184
  # early closes we expect:
155
185
  early_closes = [
156
- pd.Timestamp("2023-04-07 2:00PM", tz="America/New_York"), # Good Friday
186
+ pd.Timestamp("2023-04-07 12:00PM", tz="America/New_York"), # Good Friday (12pm because it's the first Friday)
187
+ # No early close the day before Good Friday
157
188
  pd.Timestamp("2023-05-26 2:00PM", tz="America/New_York"), # Day before Memorial Day
158
189
  pd.Timestamp("2023-07-03 2:00PM", tz="America/New_York"), # Day before Independence Day
159
190
  pd.Timestamp("2023-11-24 2:00PM", tz="America/New_York"), # Day after Thanksgiving
@@ -166,10 +197,11 @@ def test_us_2023():
166
197
  def test_us_2022():
167
198
  start = "2022-01-01"
168
199
  end = "2022-12-31"
200
+ # Note: Good Friday 2022-04-15 is NOT the first Friday -> full holiday
169
201
  holidays = [
170
202
  pd.Timestamp("2022-01-17", tz="UTC"), # MLK
171
203
  pd.Timestamp("2022-02-21", tz="UTC"), # Presidents Day
172
- pd.Timestamp("2022-04-15", tz="UTC"), # Good Friday
204
+ pd.Timestamp("2022-04-15", tz="UTC"), # Good Friday (Full Holiday)
173
205
  pd.Timestamp("2022-05-30", tz="UTC"), # Memorial Day
174
206
  pd.Timestamp("2022-06-20", tz="UTC"), # Juneteenth
175
207
  pd.Timestamp("2022-07-04", tz="UTC"), # Independence Day
@@ -184,7 +216,9 @@ def test_us_2022():
184
216
 
185
217
  # early closes we expect:
186
218
  early_closes = [
187
- pd.Timestamp("2022-04-14 2:00PM", tz="America/New_York"), # Day before Good Friday
219
+ pd.Timestamp(
220
+ "2022-04-14 2:00PM", tz="America/New_York"
221
+ ), # Day before Good Friday (2pm because GF is full holiday)
188
222
  pd.Timestamp("2022-05-27 2:00PM", tz="America/New_York"), # Day before Memorial Day
189
223
  pd.Timestamp("2022-07-01 2:00PM", tz="America/New_York"), # Day before Independence Day
190
224
  pd.Timestamp("2022-11-25 2:00PM", tz="America/New_York"), # Day after Thanksgiving
@@ -197,24 +231,28 @@ def test_us_2022():
197
231
  def test_us_2021():
198
232
  start = "2021-01-01"
199
233
  end = "2021-12-31"
234
+ # Note: Good Friday 2021-04-02 IS the first Friday -> 12pm early close
200
235
  holidays = [
201
236
  pd.Timestamp("2021-01-01", tz="UTC"), # New Year's Day
202
237
  pd.Timestamp("2021-01-18", tz="UTC"), # MLK
203
238
  pd.Timestamp("2021-02-15", tz="UTC"), # Presidents Day
239
+ # Good Friday is NOT a full holiday
204
240
  pd.Timestamp("2021-05-31", tz="UTC"), # Memorial Day
205
- pd.Timestamp("2021-07-05", tz="UTC"), # Independence Day
241
+ # Juneteenth not observed by SIFMA in 2021
242
+ pd.Timestamp("2021-07-05", tz="UTC"), # Independence Day observed
206
243
  pd.Timestamp("2021-09-06", tz="UTC"), # Labor Day
207
244
  pd.Timestamp("2021-10-11", tz="UTC"), # Columbus Day
208
245
  pd.Timestamp("2021-11-11", tz="UTC"), # Veterans Day
209
246
  pd.Timestamp("2021-11-25", tz="UTC"), # Thanksgiving
210
- pd.Timestamp("2021-12-24", tz="UTC"), # Christmas
247
+ pd.Timestamp("2021-12-24", tz="UTC"), # Christmas observed
211
248
  ]
212
249
  _test_holidays(sifma_us, holidays, start, end)
213
250
  _test_no_special_opens(sifma_us, start, end)
214
251
 
215
252
  # early closes we expect:
216
253
  early_closes = [
217
- pd.Timestamp("2021-04-02 2:00PM", tz="America/New_York"), # Day before Good Friday
254
+ pd.Timestamp("2021-04-02 12:00PM", tz="America/New_York"), # Good Friday (12pm because it's the first Friday)
255
+ # No early close the day before Good Friday
218
256
  pd.Timestamp("2021-05-28 2:00PM", tz="America/New_York"), # Day before Memorial Day
219
257
  pd.Timestamp("2021-07-02 2:00PM", tz="America/New_York"), # Day before Independence Day
220
258
  pd.Timestamp("2021-11-26 2:00PM", tz="America/New_York"), # Day after Thanksgiving
@@ -286,6 +324,75 @@ def test_us_2019():
286
324
  _test_has_early_closes(sifma_us, early_closes, start, end)
287
325
 
288
326
 
327
+ def test_us_2026():
328
+ start = "2026-01-01"
329
+ end = "2026-12-31"
330
+ # Note: Good Friday 2026-04-03 IS the first Friday -> 12pm early close
331
+ holidays = [
332
+ pd.Timestamp("2026-01-01", tz="UTC"), # New Year's Day
333
+ pd.Timestamp("2026-01-19", tz="UTC"), # MLK
334
+ pd.Timestamp("2026-02-16", tz="UTC"), # Presidents Day
335
+ # Good Friday is NOT a full holiday
336
+ pd.Timestamp("2026-05-25", tz="UTC"), # Memorial Day
337
+ pd.Timestamp("2026-06-19", tz="UTC"), # Juneteenth
338
+ pd.Timestamp("2026-07-03", tz="UTC"), # Independence Day observed
339
+ pd.Timestamp("2026-09-07", tz="UTC"), # Labor Day
340
+ pd.Timestamp("2026-10-12", tz="UTC"), # Columbus Day
341
+ pd.Timestamp("2026-11-11", tz="UTC"), # Veterans Day
342
+ pd.Timestamp("2026-11-26", tz="UTC"), # Thanksgiving
343
+ pd.Timestamp("2026-12-25", tz="UTC"), # Christmas
344
+ ]
345
+ _test_holidays(sifma_us, holidays, start, end)
346
+ _test_no_special_opens(sifma_us, start, end)
347
+
348
+ # early closes we expect:
349
+ early_closes = [
350
+ pd.Timestamp("2026-04-03 12:00PM", tz="America/New_York"), # Good Friday (12pm because it's the first Friday)
351
+ # No early close the day before Good Friday
352
+ pd.Timestamp("2026-05-22 2:00PM", tz="America/New_York"), # Day before Memorial Day
353
+ pd.Timestamp("2026-07-02 2:00PM", tz="America/New_York"), # Day before Independence Day
354
+ pd.Timestamp("2026-11-27 2:00PM", tz="America/New_York"), # Day after Thanksgiving
355
+ pd.Timestamp("2026-12-24 2:00PM", tz="America/New_York"), # Day before Christmas
356
+ pd.Timestamp("2026-12-31 2:00PM", tz="America/New_York"), # New Year's Eve
357
+ ]
358
+ _test_has_early_closes(sifma_us, early_closes, start, end)
359
+
360
+
361
+ def test_us_2027():
362
+ start = "2027-01-01"
363
+ end = "2027-12-31"
364
+ # Note: Good Friday 2027-03-26 is NOT the first Friday -> full holiday
365
+ holidays = [
366
+ pd.Timestamp("2027-01-01", tz="UTC"), # New Year's Day
367
+ pd.Timestamp("2027-01-18", tz="UTC"), # MLK
368
+ pd.Timestamp("2027-02-15", tz="UTC"), # Presidents Day
369
+ pd.Timestamp("2027-03-26", tz="UTC"), # Good Friday (Full Holiday)
370
+ pd.Timestamp("2027-05-31", tz="UTC"), # Memorial Day
371
+ pd.Timestamp("2027-06-18", tz="UTC"), # Juneteenth observed
372
+ pd.Timestamp("2027-07-05", tz="UTC"), # Independence Day observed
373
+ pd.Timestamp("2027-09-06", tz="UTC"), # Labor Day
374
+ pd.Timestamp("2027-10-11", tz="UTC"), # Columbus Day
375
+ pd.Timestamp("2027-11-11", tz="UTC"), # Veterans Day
376
+ pd.Timestamp("2027-11-25", tz="UTC"), # Thanksgiving
377
+ pd.Timestamp("2027-12-24", tz="UTC"), # Christmas observed
378
+ ]
379
+ _test_holidays(sifma_us, holidays, start, end)
380
+ _test_no_special_opens(sifma_us, start, end)
381
+
382
+ # early closes we expect:
383
+ early_closes = [
384
+ pd.Timestamp(
385
+ "2027-03-25 2:00PM", tz="America/New_York"
386
+ ), # Day before Good Friday (2pm because GF is full holiday)
387
+ pd.Timestamp("2027-05-28 2:00PM", tz="America/New_York"), # Day before Memorial Day
388
+ pd.Timestamp("2027-07-02 2:00PM", tz="America/New_York"), # Day before Independence Day
389
+ pd.Timestamp("2027-11-26 2:00PM", tz="America/New_York"), # Day after Thanksgiving
390
+ pd.Timestamp("2027-12-23 2:00PM", tz="America/New_York"), # Day before Christmas
391
+ pd.Timestamp("2027-12-31 2:00PM", tz="America/New_York"), # New Year's Eve
392
+ ]
393
+ _test_has_early_closes(sifma_us, early_closes, start, end)
394
+
395
+
289
396
  #########################################################################
290
397
  # UK TESTS
291
398
  #########################################################################
@@ -56,9 +56,7 @@ def test_merge_schedules():
56
56
  ],
57
57
  },
58
58
  columns=["market_open", "market_close"],
59
- index=pd.DatetimeIndex(
60
- ["2016-07-01", "2016-07-04", "2016-07-05", "2016-07-06"]
61
- ),
59
+ index=pd.DatetimeIndex(["2016-07-01", "2016-07-04", "2016-07-05", "2016-07-06"]),
62
60
  )
63
61
  actual = mcal.merge_schedules([sch1, sch2], how="outer")
64
62
  assert_frame_equal(actual, expected)
@@ -230,7 +228,7 @@ def test_mark_session():
230
228
  ],
231
229
  dtype="datetime64[ns, UTC]",
232
230
  ),
233
- dtype=pd.CategoricalDtype(categories=["closed", "rth"], ordered=False),
231
+ dtype=pd.CategoricalDtype(categories=["break", "closed", "rth"], ordered=False),
234
232
  ),
235
233
  mcal.mark_session(
236
234
  sched,
@@ -264,3 +262,28 @@ def test_mark_session():
264
262
  closed="left",
265
263
  ),
266
264
  )
265
+
266
+
267
+ def test_mark_session_edge_case():
268
+ # Edge case test where mark_session needs an additional schedule row because the first
269
+ # timestamp of a given date range lands in the post market session of the day prior
270
+ NYSE = mcal.get_calendar("NYSE")
271
+ sched = NYSE.schedule("2015-12-25", "2016-01-05", market_times="all", tz="UTC")
272
+ dt = pd.date_range("2015-12-31T23:00", "2016-01-01T02:00", freq="30min", tz="UTC")
273
+
274
+ assert_series_equal(
275
+ pd.Series(
276
+ [
277
+ "post",
278
+ "post",
279
+ "post",
280
+ "post",
281
+ "closed",
282
+ "closed",
283
+ "closed",
284
+ ],
285
+ index=dt,
286
+ dtype=pd.CategoricalDtype(["closed", "post", "pre", "rth"], ordered=False),
287
+ ),
288
+ mcal.mark_session(sched, dt, closed="left"),
289
+ )