diff --git a/exchange_calendars/exchange_calendar.py b/exchange_calendars/exchange_calendar.py index e57127ac..301b6c14 100644 --- a/exchange_calendars/exchange_calendar.py +++ b/exchange_calendars/exchange_calendar.py @@ -357,11 +357,15 @@ def __init__( _special_opens = self._calculate_special_opens(start, end) _special_closes = self._calculate_special_closes(start, end) - # Overwrite the special opens and closes on top of the standard ones. - _overwrite_special_dates(_all_days, self._opens, _special_opens) - _overwrite_special_dates(_all_days, self._closes, _special_closes) - _remove_breaks_for_special_dates(_all_days, self._break_starts, _special_closes) - _remove_breaks_for_special_dates(_all_days, self._break_ends, _special_closes) + # Adjust for special opens and closes. + self._opens = _adjust_special_dates(_all_days, self._opens, _special_opens) + self._closes = _adjust_special_dates(_all_days, self._closes, _special_closes) + self._break_starts = _remove_breaks_for_special_dates( + _all_days, self._break_starts, _special_closes + ) + self._break_ends = _remove_breaks_for_special_dates( + _all_days, self._break_ends, _special_closes + ) break_starts = None if self._break_starts is None else self._break_starts break_ends = None if self._break_ends is None else self._break_ends @@ -2869,18 +2873,18 @@ def scheduled_special_times( ) -def _overwrite_special_dates( +def _adjust_special_dates( session_labels: pd.DatetimeIndex, standard_times: pd.DatetimeIndex, special_times: pd.Series, -) -> None: - """Overwrite standard times of a session bound with special times. +) -> pd.DatetimeIndex: + """Adjust standard times of a session bound with special times. `session_labels` required for alignment. """ # Short circuit when nothing to apply. if special_times.empty: - return + return standard_times len_m, len_oc = len(session_labels), len(standard_times) if len_m != len_oc: @@ -2900,21 +2904,19 @@ def _overwrite_special_dates( bad_dates = list(special_times[indexer == -1]) raise ValueError(f"Special dates {bad_dates} are not sessions.") - # NOTE: This is a slightly dirty hack. We're in-place overwriting the - # internal data of an Index, which is conceptually immutable. Since we're - # maintaining sorting, this should be ok, but this is a good place to - # sanity check if things start going haywire with calendar computations. - standard_times.values[indexer] = special_times.values + srs = standard_times.to_series() + srs.iloc[indexer] = special_times + return pd.DatetimeIndex(srs) def _remove_breaks_for_special_dates( session_labels: pd.DatetimeIndex, standard_break_times: pd.DatetimeIndex | None, special_times: pd.Series, -) -> None: +) -> pd.DatetimeIndex | None: """Remove standard break times for sessions with special times." - Overwrites standard break times with NaT for sessions with speical + Replaces standard break times with NaT for sessions with speical times. Anticipated that `special_times` will be special times for 'opens' or 'closes'. @@ -2922,11 +2924,11 @@ def _remove_breaks_for_special_dates( """ # Short circuit when we have no breaks if standard_break_times is None: - return + return None # Short circuit when nothing to apply. if special_times.empty: - return + return standard_break_times len_m, len_oc = len(session_labels), len(standard_break_times) if len_m != len_oc: @@ -2946,8 +2948,6 @@ def _remove_breaks_for_special_dates( bad_dates = list(special_times[indexer == -1]) raise ValueError(f"Special dates {bad_dates} are not trading days.") - # NOTE: This is a slightly dirty hack. We're in-place overwriting the - # internal data of an Index, which is conceptually immutable. Since we're - # maintaining sorting, this should be ok, but this is a good place to - # sanity check if things start going haywire with calendar computations. - standard_break_times.values[indexer] = NP_NAT + srs = standard_break_times.to_series() + srs.iloc[indexer] = np.nan + return pd.DatetimeIndex(srs) diff --git a/exchange_calendars/exchange_calendar_xhkg.py b/exchange_calendars/exchange_calendar_xhkg.py index 35e54637..c29f188a 100644 --- a/exchange_calendars/exchange_calendar_xhkg.py +++ b/exchange_calendars/exchange_calendar_xhkg.py @@ -232,7 +232,9 @@ def process_queen_birthday(dt): # on the Monday (2022-09-12). In the past they don't seem to have followed # this pattern. We'll have to wait and see before we generalise this into a rule. pd.Timestamp("2022-09-12"), - pd.Timestamp("2023-07-17"), # 8号台风泰利, 全天休市 https://www.hkex.com.hk/News/Market-Communications/2023/2307172news?sc_lang=en + pd.Timestamp( + "2023-07-17" + ), # 8号台风泰利, 全天休市 https://www.hkex.com.hk/News/Market-Communications/2023/2307172news?sc_lang=en ] @@ -359,7 +361,7 @@ def adhoc_holidays(self): qingming_festival = vectorized_sunday_to_monday( qingming_festival_dates, - ).values + ).values.copy() # copy so that array is writeable years = qingming_festival.astype("M8[Y]") easter_monday = EasterMonday.dates(years[0], years[-1] + 1) # qingming gets observed one day later if easter monday is on the same @@ -370,7 +372,7 @@ def adhoc_holidays(self): # conflicts with national day, then national day is observed on the # second, though we don't encode that in the regular holiday, so # instead we pretend that the mid autumn festival would be delayed. - mid_autumn_festival = day_after_mid_autumn_festival_dates.values + mid_autumn_festival = day_after_mid_autumn_festival_dates.values.copy() mid_autumn_festival[ (day_after_mid_autumn_festival_dates.month == 10) & (day_after_mid_autumn_festival_dates.day == 1) @@ -435,7 +437,7 @@ def special_closes(self): @property def special_closes_adhoc(self): lunar_new_years_eve = (chinese_lunar_new_year_dates - pd.Timedelta(days=1))[ - np.in1d( + np.isin( chinese_lunar_new_year_dates.weekday, [TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY], ) diff --git a/exchange_calendars/exchange_calendar_xkrx.py b/exchange_calendars/exchange_calendar_xkrx.py index 846e79b2..1bd6a1cb 100644 --- a/exchange_calendars/exchange_calendar_xkrx.py +++ b/exchange_calendars/exchange_calendar_xkrx.py @@ -217,7 +217,7 @@ def special_offsets_adhoc( ), ] - def _overwrite_special_offsets( + def _adjust_special_offsets( self, session_labels: pd.DatetimeIndex, standard_times: pd.DatetimeIndex | None, @@ -229,7 +229,7 @@ def _overwrite_special_offsets( ): # Short circuit when nothing to apply. if standard_times is None or not len(standard_times): - return + return standard_times len_m, len_oc = len(session_labels), len(standard_times) if len_m != len_oc: @@ -271,13 +271,11 @@ def _overwrite_special_offsets( # Short circuit when nothing to apply. if not len(special_opens_or_closes): - return + return standard_times - # NOTE: This is a slightly dirty hack. We're in-place overwriting the - # internal data of an Index, which is conceptually immutable. Since we're - # maintaining sorting, this should be ok, but this is a good place to - # sanity check if things start going haywire with calendar computations. - standard_times.values[indexer] = special_opens_or_closes.values + srs = standard_times.to_series() + srs.iloc[indexer] = special_opens_or_closes + return pd.DatetimeIndex(srs) def apply_special_offsets( self, @@ -314,7 +312,7 @@ def apply_special_offsets( (t[3], t[-1]) for t in _special_offsets_adhoc if t[3] is not None ] - self._overwrite_special_offsets( + self._opens = self._adjust_special_offsets( session_labels, self._opens, _special_open_offsets, @@ -322,7 +320,7 @@ def apply_special_offsets( start, end, ) - self._overwrite_special_offsets( + self._break_starts = self._adjust_special_offsets( session_labels, self._break_starts, _special_break_start_offsets, @@ -330,7 +328,7 @@ def apply_special_offsets( start, end, ) - self._overwrite_special_offsets( + self._break_ends = self._adjust_special_offsets( session_labels, self._break_ends, _special_break_end_offsets, @@ -338,7 +336,7 @@ def apply_special_offsets( start, end, ) - self._overwrite_special_offsets( + self._closes = self._adjust_special_offsets( session_labels, self._closes, _special_close_offsets, diff --git a/tests/test_exchange_calendar.py b/tests/test_exchange_calendar.py index c2f12f37..3ce62c71 100644 --- a/tests/test_exchange_calendar.py +++ b/tests/test_exchange_calendar.py @@ -1364,7 +1364,7 @@ def _trading_minute_to_break_minute( for session, break_session in zip(sessions[mask], break_sessions[mask]): break_minutes = self.get_session_break_minutes(break_session) trading_minutes = self.get_session_minutes(session)[0] - bv = np.in1d(trading_minutes.time, break_minutes.time) + bv = np.isin(trading_minutes.time, break_minutes.time) minutes.append([trading_minutes[bv][-1], session, break_session]) return minutes