pandas-market-calendars 5.1.0__py3-none-any.whl → 5.1.1__py3-none-any.whl

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 (48) hide show
  1. pandas_market_calendars/__init__.py +39 -39
  2. pandas_market_calendars/calendar_registry.py +57 -57
  3. pandas_market_calendars/calendar_utils.py +1151 -1151
  4. pandas_market_calendars/calendars/asx.py +77 -70
  5. pandas_market_calendars/calendars/bmf.py +226 -219
  6. pandas_market_calendars/calendars/bse.py +432 -425
  7. pandas_market_calendars/calendars/cboe.py +156 -149
  8. pandas_market_calendars/calendars/cme.py +412 -405
  9. pandas_market_calendars/calendars/cme_globex_agriculture.py +172 -172
  10. pandas_market_calendars/calendars/cme_globex_base.py +126 -119
  11. pandas_market_calendars/calendars/cme_globex_crypto.py +165 -158
  12. pandas_market_calendars/calendars/cme_globex_energy_and_metals.py +223 -216
  13. pandas_market_calendars/calendars/cme_globex_equities.py +130 -123
  14. pandas_market_calendars/calendars/cme_globex_fixed_income.py +136 -136
  15. pandas_market_calendars/calendars/cme_globex_fx.py +101 -101
  16. pandas_market_calendars/calendars/eurex.py +138 -131
  17. pandas_market_calendars/calendars/eurex_fixed_income.py +105 -98
  18. pandas_market_calendars/calendars/hkex.py +438 -431
  19. pandas_market_calendars/calendars/ice.py +88 -81
  20. pandas_market_calendars/calendars/iex.py +162 -155
  21. pandas_market_calendars/calendars/jpx.py +124 -117
  22. pandas_market_calendars/calendars/lse.py +125 -118
  23. pandas_market_calendars/calendars/mirror.py +144 -144
  24. pandas_market_calendars/calendars/nyse.py +1472 -1466
  25. pandas_market_calendars/calendars/ose.py +125 -118
  26. pandas_market_calendars/calendars/sifma.py +390 -383
  27. pandas_market_calendars/calendars/six.py +143 -136
  28. pandas_market_calendars/calendars/sse.py +322 -315
  29. pandas_market_calendars/calendars/tase.py +231 -224
  30. pandas_market_calendars/calendars/tsx.py +192 -185
  31. pandas_market_calendars/class_registry.py +115 -115
  32. pandas_market_calendars/holidays/cme.py +385 -385
  33. pandas_market_calendars/holidays/cme_globex.py +214 -214
  34. pandas_market_calendars/holidays/cn.py +1476 -1476
  35. pandas_market_calendars/holidays/jp.py +401 -401
  36. pandas_market_calendars/holidays/jpx_equinox.py +506 -506
  37. pandas_market_calendars/holidays/nyse.py +1536 -1536
  38. pandas_market_calendars/holidays/oz.py +63 -63
  39. pandas_market_calendars/holidays/sifma.py +350 -350
  40. pandas_market_calendars/holidays/us.py +376 -376
  41. pandas_market_calendars/market_calendar.py +1008 -1008
  42. {pandas_market_calendars-5.1.0.dist-info → pandas_market_calendars-5.1.1.dist-info}/METADATA +3 -1
  43. pandas_market_calendars-5.1.1.dist-info/RECORD +50 -0
  44. {pandas_market_calendars-5.1.0.dist-info → pandas_market_calendars-5.1.1.dist-info}/WHEEL +1 -1
  45. pandas_market_calendars-5.1.0.dist-info/RECORD +0 -50
  46. {pandas_market_calendars-5.1.0.dist-info → pandas_market_calendars-5.1.1.dist-info}/licenses/LICENSE +0 -0
  47. {pandas_market_calendars-5.1.0.dist-info → pandas_market_calendars-5.1.1.dist-info}/licenses/NOTICE +0 -0
  48. {pandas_market_calendars-5.1.0.dist-info → pandas_market_calendars-5.1.1.dist-info}/top_level.txt +0 -0
@@ -1,1008 +1,1008 @@
1
- # Fork of zipline from Quantopian. Licensed under MIT, original licence below
2
- #
3
- # Copyright 2016 Quantopian, Inc.
4
- #
5
- # Licensed under the Apache License, Version 2.0 (the "License");
6
- # you may not use this file except in compliance with the License.
7
- # You may obtain a copy of the License at
8
- #
9
- # http://www.apache.org/licenses/LICENSE-2.0
10
- #
11
- # Unless required by applicable law or agreed to in writing, software
12
- # distributed under the License is distributed on an "AS IS" BASIS,
13
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
- # See the License for the specific language governing permissions and
15
- # limitations under the License.
16
- import warnings
17
- from abc import ABCMeta, abstractmethod
18
- from datetime import time
19
- from typing import Literal, Union
20
-
21
- import pandas as pd
22
- from pandas.tseries.offsets import CustomBusinessDay
23
-
24
- from .class_registry import RegisteryMeta, ProtectedDict
25
-
26
- from . import calendar_utils as u
27
-
28
- MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY = range(7)
29
-
30
- WEEKMASK_ABBR = {
31
- MONDAY: "Mon",
32
- TUESDAY: "Tue",
33
- WEDNESDAY: "Wed",
34
- THURSDAY: "Thu",
35
- FRIDAY: "Fri",
36
- SATURDAY: "Sat",
37
- SUNDAY: "Sun",
38
- }
39
-
40
-
41
- class DEFAULT:
42
- pass
43
-
44
-
45
- class MarketCalendarMeta(ABCMeta, RegisteryMeta):
46
- pass
47
-
48
-
49
- class MarketCalendar(metaclass=MarketCalendarMeta):
50
- """
51
- An MarketCalendar represents the timing information of a single market or exchange.
52
- Unless otherwise noted all times are in UTC and use Pandas data structures.
53
- """
54
-
55
- regular_market_times = {
56
- "market_open": ((None, time(0)),),
57
- "market_close": ((None, time(23)),),
58
- }
59
-
60
- open_close_map = {
61
- "market_open": True,
62
- "market_close": False,
63
- "break_start": False,
64
- "break_end": True,
65
- "pre": True,
66
- "post": False,
67
- }
68
-
69
- @staticmethod
70
- def _tdelta(t, day_offset=0):
71
- try:
72
- return pd.Timedelta(
73
- days=day_offset, hours=t.hour, minutes=t.minute, seconds=t.second
74
- )
75
- except AttributeError:
76
- t, day_offset = t
77
- return pd.Timedelta(
78
- days=day_offset, hours=t.hour, minutes=t.minute, seconds=t.second
79
- )
80
-
81
- @staticmethod
82
- def _off(tple):
83
- try:
84
- return tple[2]
85
- except IndexError:
86
- return 0
87
-
88
- @classmethod
89
- def calendar_names(cls):
90
- """All Market Calendar names and aliases that can be used in "factory"
91
- :return: list(str)
92
- """
93
- return [cal for cal in cls._regmeta_class_registry.keys() if cal not in ["MarketCalendar", "TradingCalendar"]]
94
-
95
- @classmethod
96
- def factory(cls, name, *args, **kwargs): # Will be set by Meta, keeping it there for tests
97
- """
98
- :param name: The name of the MarketCalendar to be retrieved.
99
- :param *args/**kwargs: passed to requested MarketCalendar.__init__
100
- :return: MarketCalendar of the desired calendar.
101
- """
102
- return
103
-
104
- def __init__(self, open_time=None, close_time=None):
105
- """
106
- :param open_time: Market open time override as datetime.time object. If None then default is used.
107
- :param close_time: Market close time override as datetime.time object. If None then default is used.
108
- """
109
-
110
- self.regular_market_times = self.regular_market_times.copy()
111
- self.open_close_map = self.open_close_map.copy()
112
- self._customized_market_times = []
113
-
114
- if open_time is not None:
115
- self.change_time("market_open", open_time)
116
-
117
- if close_time is not None:
118
- self.change_time("market_close", close_time)
119
-
120
- if not hasattr(self, "_market_times"):
121
- self._prepare_regular_market_times()
122
-
123
- @property
124
- @abstractmethod
125
- def name(self):
126
- """
127
- Name of the market
128
-
129
- :return: string name
130
- """
131
- raise NotImplementedError()
132
-
133
- @property
134
- def full_name(self):
135
- """
136
- Full name of the market
137
-
138
- :return: string name
139
- """
140
- return self.name
141
-
142
- @property
143
- @abstractmethod
144
- def tz(self):
145
- """
146
- Time zone for the market.
147
-
148
- :return: timezone
149
- """
150
- raise NotImplementedError()
151
-
152
- @property
153
- def market_times(self):
154
- return self._market_times
155
-
156
- def _prepare_regular_market_times(self):
157
- oc_map = self.open_close_map
158
- assert all(isinstance(x, bool) for x in oc_map.values()), "Values in open_close_map need to be True or False"
159
-
160
- regular = self.regular_market_times
161
- discontinued = ProtectedDict()
162
- regular_tds = {}
163
-
164
- for market_time, times in regular.items():
165
- # in case a market_time has been discontinued, extend the last time
166
- # and add it to the discontinued_market_times dictionary
167
- if market_time.startswith("interruption_"):
168
- raise ValueError("'interruption_' prefix is reserved")
169
-
170
- if times[-1][1] is None:
171
- discontinued._set(market_time, times[-1][0])
172
- times = times[:-1]
173
- regular._set(market_time, times)
174
-
175
- regular_tds[market_time] = tuple((t[0], self._tdelta(t[1], self._off(t))) for t in times)
176
-
177
- if discontinued:
178
- warnings.warn(
179
- f"{list(discontinued.keys())} are discontinued, the dictionary"
180
- f" `.discontinued_market_times` has the dates on which these were discontinued."
181
- f" The times as of those dates are incorrect, use .remove_time(market_time)"
182
- f" to ignore a market_time."
183
- )
184
-
185
- self.discontinued_market_times = discontinued
186
- self.regular_market_times = regular
187
-
188
- self._regular_market_timedeltas = regular_tds
189
- self._market_times = sorted(regular.keys(), key=lambda x: regular_tds[x][-1][1])
190
- self._oc_market_times = list(filter(oc_map.__contains__, self._market_times))
191
-
192
- def _set_time(self, market_time, times, opens):
193
- if isinstance(times, (tuple, list)): # passed a tuple
194
- if not isinstance(times[0], (tuple, list)): # doesn't have a tuple inside
195
- if times[0] is None: # seems to be a tuple indicating starting time
196
- times = (times,)
197
- else: # must be a tuple with: (time, offset)
198
- times = ((None, times[0], times[1]),)
199
- else: # should be a datetime.time object
200
- times = ((None, times),)
201
-
202
- ln = len(times)
203
- for i, t in enumerate(times):
204
- try:
205
- assert t[0] is None or isinstance(t[0], str) or isinstance(t[0], pd.Timestamp)
206
- assert isinstance(t[1], time) or (ln > 1 and i == ln - 1 and t[1] is None)
207
- assert isinstance(self._off(t), int)
208
- except AssertionError:
209
- raise AssertionError(
210
- "The passed time information is not in the right format, "
211
- "please consult the docs for how to set market times"
212
- )
213
-
214
- if opens is DEFAULT:
215
- opens = self.__class__.open_close_map.get(market_time, None)
216
-
217
- if opens in (True, False):
218
- self.open_close_map._set(market_time, opens)
219
-
220
- elif opens is None: # make sure it's ignored
221
- try:
222
- self.open_close_map._del(market_time)
223
- except KeyError:
224
- pass
225
- else:
226
- raise ValueError("when you pass `opens`, it needs to be True, False, or None")
227
-
228
- self.regular_market_times._set(market_time, times)
229
-
230
- if not self.is_custom(market_time):
231
- self._customized_market_times.append(market_time)
232
-
233
- self._prepare_regular_market_times()
234
-
235
- def change_time(self, market_time, times, opens=DEFAULT):
236
- """
237
- Changes the specified market time in regular_market_times and makes the necessary adjustments.
238
-
239
- :param market_time: the market_time to change
240
- :param times: new time information
241
- :param opens: whether the market_time is a time that closes or opens the market
242
- this is only needed if the market_time should be respected by .open_at_time
243
- True: opens
244
- False: closes
245
- None: consider it neither opening nor closing, don't add to open_close_map (ignore in .open_at_time)
246
- DEFAULT: same as None, unless the market_time is in self.__class__.open_close_map. Then it will take
247
- the default value as defined by the class.
248
- :return: None
249
- """
250
- assert market_time in self.regular_market_times, (
251
- f"{market_time} is not in regular_market_times:" f"\n{self._market_times}."
252
- )
253
- return self._set_time(market_time, times, opens)
254
-
255
- def add_time(self, market_time, times, opens=DEFAULT):
256
- """
257
- Adds the specified market time to regular_market_times and makes the necessary adjustments.
258
-
259
- :param market_time: the market_time to add
260
- :param times: the time information
261
- :param opens: see .change_time docstring
262
- :return: None
263
- """
264
- assert market_time not in self.regular_market_times, (
265
- f"{market_time} is already in regular_market_times:" f"\n{self._market_times}"
266
- )
267
-
268
- return self._set_time(market_time, times, opens)
269
-
270
- def remove_time(self, market_time):
271
- """
272
- Removes the specified market time from regular_market_times and makes the necessary adjustments.
273
-
274
- :param market_time: the market_time to remove
275
- :return: None
276
- """
277
-
278
- self.regular_market_times._del(market_time)
279
- try:
280
- self.open_close_map._del(market_time)
281
- except KeyError:
282
- pass
283
-
284
- self._prepare_regular_market_times()
285
- if self.is_custom(market_time):
286
- self._customized_market_times.remove(market_time)
287
-
288
- def is_custom(self, market_time):
289
- return market_time in self._customized_market_times
290
-
291
- @property
292
- def has_custom(self):
293
- return len(self._customized_market_times) > 0
294
-
295
- def is_discontinued(self, market_time):
296
- return market_time in self.discontinued_market_times
297
-
298
- @property
299
- def has_discontinued(self):
300
- return len(self.discontinued_market_times) > 0
301
-
302
- def get_time(self, market_time, all_times=False):
303
- try:
304
- times = self.regular_market_times[market_time]
305
- except KeyError as e:
306
- if "break_start" in market_time or "break_end" in market_time:
307
- return None # in case of no breaks
308
- elif market_time in ["market_open", "market_close"]:
309
- raise NotImplementedError("You need to set market_times")
310
- else:
311
- raise e
312
-
313
- if all_times:
314
- return times
315
- return times[-1][1].replace(tzinfo=self.tz)
316
-
317
- def get_time_on(self, market_time, date):
318
- times = self.get_time(market_time, all_times=True)
319
- if times is None:
320
- return None
321
-
322
- date = pd.Timestamp(date)
323
- for d, t in times[::-1]:
324
- if d is None or pd.Timestamp(d) < date:
325
- return t.replace(tzinfo=self.tz)
326
-
327
- def open_time_on(self, date):
328
- return self.get_time_on("market_open", date)
329
-
330
- def close_time_on(self, date):
331
- return self.get_time_on("market_close", date)
332
-
333
- def break_start_on(self, date):
334
- return self.get_time_on("break_start", date)
335
-
336
- def break_end_on(self, date):
337
- return self.get_time_on("break_end", date)
338
-
339
- @property
340
- def open_time(self):
341
- """
342
- Default open time for the market
343
-
344
- :return: time
345
- """
346
- return self.get_time("market_open")
347
-
348
- @property
349
- def close_time(self):
350
- """
351
- Default close time for the market
352
-
353
- :return: time
354
- """
355
- return self.get_time("market_close")
356
-
357
- @property
358
- def break_start(self):
359
- """
360
- Break time start. If None then there is no break
361
-
362
- :return: time or None
363
- """
364
- return self.get_time("break_start")
365
-
366
- @property
367
- def break_end(self):
368
- """
369
- Break time end. If None then there is no break
370
-
371
- :return: time or None
372
- """
373
- return self.get_time("break_end")
374
-
375
- @property
376
- def regular_holidays(self):
377
- """
378
-
379
- :return: pd.AbstractHolidayCalendar: a calendar containing the regular holidays for this calendar
380
- """
381
- return None
382
-
383
- @property
384
- def adhoc_holidays(self):
385
- """
386
-
387
- :return: list of ad-hoc holidays
388
- """
389
- return []
390
-
391
- @property
392
- def weekmask(self):
393
- return "Mon Tue Wed Thu Fri"
394
-
395
- @property
396
- def special_opens(self):
397
- """
398
- A list of special open times and corresponding AbstractHolidayCalendar.
399
-
400
- :return: List of (time, AbstractHolidayCalendar) tuples
401
- """
402
- return []
403
-
404
- @property
405
- def special_opens_adhoc(self):
406
- """
407
-
408
- :return: List of (time, DatetimeIndex) tuples that represent special opens that cannot be codified into rules.
409
- """
410
- return []
411
-
412
- @property
413
- def special_closes(self):
414
- """
415
- A list of special close times and corresponding HolidayCalendars.
416
-
417
- :return: List of (time, AbstractHolidayCalendar) tuples
418
- """
419
- return []
420
-
421
- @property
422
- def special_closes_adhoc(self):
423
- """
424
-
425
- :return: List of (time, DatetimeIndex) tuples that represent special closes that cannot be codified into rules.
426
- """
427
- return []
428
-
429
- def get_special_times(self, market_time):
430
- return getattr(self, "special_" + market_time, [])
431
-
432
- def get_special_times_adhoc(self, market_time):
433
- return getattr(self, "special_" + market_time + "_adhoc", [])
434
-
435
- def get_offset(self, market_time):
436
- return self._off(self.get_time(market_time, all_times=True)[-1])
437
-
438
- @property
439
- def open_offset(self):
440
- """
441
- :return: open offset
442
- """
443
- return self.get_offset("market_open")
444
-
445
- @property
446
- def close_offset(self):
447
- """
448
- :return: close offset
449
- """
450
- return self.get_offset("market_close")
451
-
452
- @property
453
- def interruptions(self):
454
- """
455
- This needs to be a list with a tuple for each date that had an interruption.
456
- The tuple should have this layout:
457
-
458
- (date, start_time, end_time[, start_time2, end_time2, ...])
459
-
460
- E.g.:
461
- [
462
- ("2002-02-03", (time(11), -1), time(11, 2)),
463
- ("2010-01-11", time(11), (time(11, 1), 1)),
464
- ("2010-01-13", time(9, 59), time(10), time(10, 29), time(10, 30)),
465
- ("2011-01-10", time(11), time(11, 1))
466
- ]
467
-
468
- The date needs to be a string in this format: 'yyyy-mm-dd'.
469
- Times need to be two datetime.time objects for each interruption, indicating start and end.
470
- Optionally these can be wrapped in a tuple, where the
471
- second element needs to be an integer indicating an offset.
472
- On "2010-01-13" in the example, it is shown that there can be multiple interruptions in a day.
473
- """
474
- return []
475
-
476
- def _convert(self, col: pd.Series):
477
- """
478
- col is a series indexed by dates at which interruptions occurred. The values are either the start or end times
479
- of an interruption, represented by either a timedelta or a tuple with a timedelta and day offset of the form
480
- (timedelta, offset). _convert produces a new series where the values are replaced by datetimes equal to the
481
- index of the original series plus the offset if present, at the timedelta.
482
-
483
- E.g.:
484
- >>> self._convert(
485
- pd.Series(
486
- [datetime.time(11, 2), (datetime.time(11, 1), 1), datetime.time(10, 0), None],
487
- index=pd.DatetimeIndex(['2002-02-03', '2010-01-11', '2010-01-13', '2011-01-10'])
488
- )
489
- )
490
- 2002-02-03 2002-02-03 11:02:00+00:00
491
- 2010-01-11 2010-01-12 11:01:00+00:00
492
- 2010-01-13 2010-01-13 10:00:00+00:00
493
- 2011-01-10 NaT
494
- dtype: datetime64[ns, UTC]
495
- """
496
- col = col.dropna() # Python 3.8, pandas 2.0.3 cannot create time deltas from NaT
497
- try:
498
- times = col.str[0]
499
- except AttributeError: # no tuples, only offset 0
500
- return (
501
- (pd.to_timedelta(col.astype("string").fillna(""), errors="coerce") + col.index)
502
- .dt.tz_localize(self.tz)
503
- .dt.tz_convert("UTC")
504
- )
505
-
506
- return (
507
- (
508
- pd.to_timedelta(times.fillna(col).astype("string").fillna(""), errors="coerce")
509
- + pd.to_timedelta(col.str[1].fillna(0), unit="D")
510
- + col.index
511
- )
512
- .dt.tz_localize(self.tz)
513
- .dt.tz_convert("UTC")
514
- )
515
-
516
- @staticmethod
517
- def _col_name(n: int):
518
- return f"interruption_start_{n // 2 + 1}" if n % 2 == 1 else f"interruption_end_{n // 2}"
519
-
520
- @property
521
- def interruptions_df(self):
522
- """
523
- Will return a pd.DataFrame only containing interruptions.
524
- """
525
- if not self.interruptions:
526
- return pd.DataFrame(index=pd.DatetimeIndex([]))
527
- intr = pd.DataFrame(self.interruptions)
528
- intr.index = pd.to_datetime(intr.pop(0))
529
-
530
- intr.columns = map(self._col_name, intr.columns)
531
- intr.index.name = None
532
-
533
- return intr.apply(self._convert).sort_index()
534
-
535
- def holidays(self) -> pd.tseries.offsets.CustomBusinessDay:
536
- """
537
- Returns the complete CustomBusinessDay object of holidays that can be used in any Pandas function that take
538
- that input.
539
-
540
- :return: CustomBusinessDay object of holidays
541
- """
542
- try:
543
- return self._holidays
544
- except AttributeError:
545
- self._holidays = CustomBusinessDay(
546
- holidays=self.adhoc_holidays,
547
- calendar=self.regular_holidays,
548
- weekmask=self.weekmask,
549
- )
550
- return self._holidays
551
-
552
- def valid_days(self, start_date, end_date, tz="UTC") -> pd.DatetimeIndex:
553
- """
554
- Get a DatetimeIndex of valid open business days.
555
-
556
- :param start_date: start date
557
- :param end_date: end date
558
- :param tz: time zone in either string or pytz.timezone
559
- :return: DatetimeIndex of valid business days
560
- """
561
- return pd.date_range(start_date, end_date, freq=self.holidays(), normalize=True, tz=tz)
562
-
563
- def _get_market_times(self, start, end):
564
- mts = self._market_times
565
- return mts[mts.index(start) : mts.index(end) + 1]
566
-
567
- def days_at_time(self, days, market_time, day_offset=0):
568
- """
569
- Create an index of days at time ``t``, interpreted in timezone ``tz``. The returned index is localized to UTC.
570
-
571
- In the example below, the times switch from 13:45 to 12:45 UTC because
572
- March 13th is the daylight savings transition for US/Eastern. All the
573
- times are still 8:45 when interpreted in US/Eastern.
574
-
575
- >>> import pandas as pd; import datetime; import pprint
576
- >>> dts = pd.date_range('2016-03-12', '2016-03-14')
577
- >>> dts_at_845 = days_at_time(dts, datetime.time(8, 45), 'US/Eastern')
578
- >>> pprint.pprint([str(dt) for dt in dts_at_845])
579
- ['2016-03-12 13:45:00+00:00',
580
- '2016-03-13 12:45:00+00:00',
581
- '2016-03-14 12:45:00+00:00']
582
-
583
- :param days: DatetimeIndex An index of dates (represented as midnight).
584
- :param market_time: datetime.time The time to apply as an offset to each day in ``days``.
585
- :param day_offset: int The number of days we want to offset @days by
586
- :return: pd.Series of date with the time requested.
587
- """
588
- # Offset days without tz to avoid timezone issues.
589
- days = pd.DatetimeIndex(days).tz_localize(None).to_series()
590
-
591
- if isinstance(market_time, str): # if string, assume its a reference to saved market times
592
- timedeltas = self._regular_market_timedeltas[market_time]
593
- datetimes = days + timedeltas[0][1]
594
- for cut_off, timedelta in timedeltas[1:]:
595
- datetimes = datetimes.where(days < pd.Timestamp(cut_off), days + timedelta)
596
-
597
- else: # otherwise, assume it is a datetime.time object
598
- datetimes = days + self._tdelta(market_time, day_offset)
599
-
600
- return datetimes.dt.tz_localize(self.tz).dt.tz_convert("UTC")
601
-
602
- def _tryholidays(self, cal, s, e):
603
- try:
604
- # If the Calendar is all single Observance Holidays then it is far
605
- # more efficient to extract and return those dates
606
- observed_dates = u.all_single_observance_rules(cal)
607
- if observed_dates is not None:
608
- return pd.DatetimeIndex([date for date in observed_dates if s <= date <= e])
609
- else:
610
- return cal.holidays(s, e)
611
- except ValueError:
612
- return pd.DatetimeIndex([])
613
-
614
- def _special_dates(self, calendars, ad_hoc_dates, start, end):
615
- """
616
- Union an iterable of pairs of the forms (time, calendar),
617
- (time, [dates]), and (time, int). If the second item in the pair
618
- is an int it will be interpreted as a specific day of the week.
619
-
620
- (This is shared logic for computing special opens and special closes.)
621
- """
622
- indexes = []
623
- for time_, calendar in calendars:
624
- if isinstance(calendar, int):
625
- day_of_week = CustomBusinessDay(weekmask=WEEKMASK_ABBR[calendar])
626
- indexes.append(self.days_at_time(pd.date_range(start, end, freq=day_of_week), time_))
627
- else:
628
- indexes.append(self.days_at_time(self._tryholidays(calendar, start, end), time_))
629
-
630
- indexes += [self.days_at_time(dates, time_) for time_, dates in ad_hoc_dates]
631
-
632
- if indexes:
633
- dates = pd.concat(indexes).sort_index().drop_duplicates()
634
- return dates.loc[start : end.replace(hour=23, minute=59, second=59)]
635
-
636
- return pd.Series([], dtype="datetime64[ns, UTC]", index=pd.DatetimeIndex([]))
637
-
638
- def special_dates(self, market_time, start_date, end_date, filter_holidays=True):
639
- """
640
- Calculate a datetimeindex that only contains the specail times of the requested market time.
641
-
642
- :param market_time: market_time reference
643
- :param start_date: first possible date of the index
644
- :param end_date: last possible date of the index
645
- :param filter_holidays: will filter days by self.valid_days, which can be useful when debugging
646
-
647
- :return: schedule DatetimeIndex
648
- """
649
- start_date, end_date = self.clean_dates(start_date, end_date)
650
- calendars = self.get_special_times(market_time)
651
- ad_hoc = self.get_special_times_adhoc(market_time)
652
- special = self._special_dates(calendars, ad_hoc, start_date, end_date)
653
-
654
- if filter_holidays:
655
- valid = self.valid_days(start_date, end_date, tz=None)
656
- special = special[special.index.isin(valid)] # some sources of special times don't exclude holidays
657
- return special
658
-
659
- def schedule(
660
- self,
661
- start_date,
662
- end_date,
663
- tz="UTC",
664
- start="market_open",
665
- end="market_close",
666
- force_special_times=True,
667
- market_times=None,
668
- interruptions=False,
669
- ) -> pd.DataFrame:
670
- """
671
- Generates the schedule DataFrame. The resulting DataFrame will have all the valid business days as the index
672
- and columns for the requested market times. The columns can be determined either by setting a range (inclusive
673
- on both sides), using `start` and `end`, or by passing a list to `market_times'. A range of market_times is
674
- derived from a list of market_times that are available to the instance, which are sorted based on the current
675
- regular time. See examples/usage.ipynb for demonstrations.
676
-
677
- All time zones are set to UTC by default. Setting the tz parameter will convert the columns to the desired
678
- timezone, such as 'America/New_York'.
679
-
680
- :param start_date: first date of the schedule
681
- :param end_date: last date of the schedule
682
- :param tz: timezone that the columns of the returned schedule are in, default: "UTC"
683
- :param start: the first market_time to include as a column, default: "market_open"
684
- :param end: the last market_time to include as a column, default: "market_close"
685
- :param force_special_times: how to handle special times.
686
- True: overwrite regular times of the column itself, conform other columns to special times of
687
- market_open/market_close if those are requested.
688
- False: only overwrite regular times of the column itself, leave others alone
689
- None: completely ignore special times
690
- :param market_times: alternative to start/end, list of market_times that are in self.regular_market_times
691
- :param interruptions: bool, whether to add interruptions to the schedule, default: False
692
- These will be added as columns to the right of the DataFrame. Any interruption on a day between
693
- start_date and end_date will be included, regardless of the market_times requested.
694
- Also, `force_special_times` does not take these into consideration.
695
- :return: schedule DataFrame
696
- """
697
- start_date, end_date = self.clean_dates(start_date, end_date)
698
- if not (start_date <= end_date):
699
- raise ValueError("start_date must be before or equal to end_date.")
700
-
701
- _all_days = self.valid_days(start_date, end_date)
702
-
703
- # Setup all valid trading days and the requested market_times
704
- if market_times is None:
705
- market_times = self._get_market_times(start, end)
706
- elif market_times == "all":
707
- market_times = self._market_times
708
-
709
- if not _all_days.size: # If no valid days return an empty DataFrame
710
- return pd.DataFrame(columns=market_times, index=pd.DatetimeIndex([], freq="C"))
711
-
712
- return self.schedule_from_days(_all_days, tz, start, end, force_special_times, market_times, interruptions)
713
-
714
- def schedule_from_days(
715
- self,
716
- days: pd.DatetimeIndex,
717
- tz="UTC",
718
- start="market_open",
719
- end="market_close",
720
- force_special_times=True,
721
- market_times=None,
722
- interruptions=False,
723
- ) -> pd.DataFrame:
724
- """
725
- Generates a schedule DataFrame for the days provided. The days are assumed to be valid trading days.
726
-
727
- The columns can be determined either by setting a range (inclusive on both sides), using `start` and `end`,
728
- or by passing a list to `market_times'. A range of market_times is derived from a list of market_times that
729
- are available to the instance, which are sorted based on the current regular time.
730
- See examples/usage.ipynb for demonstrations.
731
-
732
- All time zones are set to UTC by default. Setting the tz parameter will convert the columns to the desired
733
- timezone, such as 'America/New_York'.
734
-
735
- :param days: pd.DatetimeIndex of all the desired days in ascending order. This function does not double check
736
- that these are valid trading days, it is assumed they are. It is intended that this parameter is generated
737
- by either the .valid_days() or .date_range_htf() methods. Time & Timezone Information is ignored.
738
- :param tz: timezone that the columns of the returned schedule are in, default: "UTC"
739
- :param start: the first market_time to include as a column, default: "market_open"
740
- :param end: the last market_time to include as a column, default: "market_close"
741
- :param force_special_times: how to handle special times.
742
- True: overwrite regular times of the column itself, conform other columns to special times of
743
- market_open/market_close if those are requested.
744
- False: only overwrite regular times of the column itself, leave others alone
745
- None: completely ignore special times
746
- :param market_times: alternative to start/end, list of market_times that are in self.regular_market_times
747
- :param interruptions: bool, whether to add interruptions to the schedule, default: False
748
- These will be added as columns to the right of the DataFrame. Any interruption on a day between
749
- start_date and end_date will be included, regardless of the market_times requested.
750
- Also, `force_special_times` does not take these into consideration.
751
- :return: schedule DataFrame
752
- """
753
-
754
- if days.dtype != "datetime64[ns]":
755
- days = pd.DatetimeIndex(days).normalize().tz_localize(None)
756
-
757
- # Setup all valid trading days and the requested market_times
758
- if market_times is None:
759
- market_times = self._get_market_times(start, end)
760
- elif market_times == "all":
761
- market_times = self._market_times
762
-
763
- _adj_others = force_special_times is True
764
- _adj_col = force_special_times is not None
765
- _open_adj = _close_adj = []
766
-
767
- schedule = pd.DataFrame()
768
- for market_time in market_times:
769
- temp = self.days_at_time(days, market_time).copy() # standard times
770
- if _adj_col:
771
- # create an array of special times
772
- special = self.special_dates(market_time, days[0], days[-1], filter_holidays=False)
773
- # overwrite standard times
774
- specialix = special.index[
775
- special.index.isin(temp.index)
776
- ] # some sources of special times don't exclude holidays
777
- temp.loc[specialix] = special
778
-
779
- if _adj_others:
780
- if market_time == "market_open":
781
- _open_adj = specialix
782
- elif market_time == "market_close":
783
- _close_adj = specialix
784
-
785
- schedule[market_time] = temp
786
-
787
- cols = schedule.columns
788
- if _adj_others and len(_open_adj) > 0:
789
- mkt_open_ind = cols.get_loc("market_open")
790
-
791
- # Can't use Lambdas here since numpy array assignment doesn't return the array.
792
- def adjust_opens(x): # x is an np.Array.
793
- x[x <= x[mkt_open_ind]] = x[mkt_open_ind]
794
- return x
795
-
796
- adjusted = schedule.loc[_open_adj].apply(adjust_opens, axis=1, raw=True)
797
- schedule.loc[_open_adj] = adjusted
798
-
799
- if _adj_others and len(_close_adj) > 0:
800
- mkt_close_ind = cols.get_loc("market_close")
801
-
802
- def adjust_closes(x):
803
- x[x >= x[mkt_close_ind]] = x[mkt_close_ind]
804
- return x
805
-
806
- adjusted = schedule.loc[_close_adj].apply(adjust_closes, axis=1, raw=True)
807
- schedule.loc[_close_adj] = adjusted
808
-
809
- if interruptions:
810
- interrs = self.interruptions_df
811
- schedule[interrs.columns] = interrs
812
- schedule = schedule.dropna(how="all", axis=1)
813
-
814
- if tz != "UTC":
815
- schedule = schedule.apply(lambda s: s.dt.tz_convert(tz))
816
-
817
- return schedule
818
-
819
- def date_range_htf(
820
- self,
821
- frequency: Union[str, pd.Timedelta, int, float],
822
- start: Union[str, pd.Timestamp, int, float, None] = None,
823
- end: Union[str, pd.Timestamp, int, float, None] = None,
824
- periods: Union[int, None] = None,
825
- closed: Union[Literal["left", "right"], None] = "right",
826
- *,
827
- day_anchor: u.Day_Anchor = "SUN",
828
- month_anchor: u.Month_Anchor = "JAN",
829
- ) -> pd.DatetimeIndex:
830
- """
831
- Returns a Normalized DatetimeIndex from the start-date to End-Date for Time periods of 1D and Higher.
832
-
833
- PARAMETERS:
834
-
835
- :param frequency: String, Int/float (POSIX seconds) or pd.Timedelta of the desired frequency.
836
- :Must be Greater than '1D' and an integer multiple of the base frequency (D, W, M, Q, or Y)
837
- :Important Note: Ints/Floats & Timedeltas are always considered as 'Open Business Days',
838
- '2D' == Every Other Buisness Day, '3D' == Every 3rd B.Day, '7D' == Every 7th B.Day
839
- :Higher periods (passed as strings) align to the beginning or end of the relevant period
840
- :i.e. '1W' == First/[Last] Trading Day of each Week, '1Q' == First/[Last] Day of every Quarter
841
-
842
- :param start: String, Int/float (POSIX seconds) or pd.Timestamp of the desired start time.
843
- :The Time & Timezone information is ignored. Only the Normalized Day is considered.
844
-
845
- :param end: String, Int/float (POSIX seconds) or pd.Timestamp of the desired start time.
846
- :The Time & Timezone information is ignored. Only the Normalized Day is considered.
847
-
848
- :param periods: Optional Integer number of periods to return. If a Period count, Start time,
849
- and End time are given the period count is ignored.
850
-
851
- :param closed: Literal['left', 'right']. Method used to close each range.
852
- :Left: First open trading day of the Session is returned (e.g. First Open Day of The Month)
853
- :right: Last open trading day of the Session is returned (e.g. Last Open Day of The Month)
854
- :Note, This has no effect when the desired frequency is a number of days.
855
-
856
- :param day_anchor: Day to Anchor the start of the Weekly timeframes to. Default 'SUN'.
857
- : To get the First/Last Days of the trading Week then the Anchor needs to be on a day the relevant
858
- market is closed.
859
- : This can be set so that a specific day each week is returned.
860
- : freq='1W' & day_anchor='WED' Will return Every 'WED' when the market is open, and nearest day
861
- to the left or right (based on 'closed') when the market is closed.
862
- Options: ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"]
863
-
864
- :param month_anchor: Month to Anchor the start of the year to for Quarter and yearly timeframes.
865
- : Default 'JAN' for Calendar Quarters/Years. Can be set to 'JUL' to return Fiscal Years
866
- Options: ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"]
867
- """
868
- return u.date_range_htf(
869
- self.holidays(),
870
- frequency,
871
- start,
872
- end,
873
- periods,
874
- closed,
875
- day_anchor=day_anchor,
876
- month_anchor=month_anchor,
877
- )
878
-
879
- def open_at_time(self, schedule, timestamp, include_close=False, only_rth=False):
880
- """
881
- Determine if a given timestamp is during an open time for the market. If the timestamp is
882
- before the first open time or after the last close time of `schedule`, a ValueError will be raised.
883
-
884
- :param schedule: schedule DataFrame
885
- :param timestamp: the timestamp to check for. Assumed to be UTC, if it doesn't include tz information.
886
- :param include_close: if False then the timestamp that equals the closing timestamp will return False and not be
887
- considered a valid open date and time. If True then it will be considered valid and return True. Use True
888
- if using bars and would like to include the last bar as a valid open date and time. The close refers to the
889
- latest market_time available, which could be after market_close (e.g. 'post').
890
- :param only_rth: whether to ignore columns that are before market_open or after market_close. If true,
891
- include_close will be referring to market_close.
892
- :return: True if the timestamp is a valid open date and time, False if not
893
- """
894
- timestamp = pd.Timestamp(timestamp)
895
- try:
896
- timestamp = timestamp.tz_localize("UTC")
897
- except TypeError:
898
- pass
899
-
900
- cols = schedule.columns
901
- interrs = cols.str.startswith("interruption_")
902
- if not (cols.isin(self._oc_market_times) | interrs).all():
903
- raise ValueError(
904
- "You seem to be using a schedule that isn't based on the market_times, "
905
- "or includes market_times that are not represented in the open_close_map."
906
- )
907
-
908
- if only_rth:
909
- lowest, highest = "market_open", "market_close"
910
- else:
911
- cols = cols[~interrs]
912
- ix = cols.map(self._oc_market_times.index)
913
- lowest, highest = cols[ix == ix.min()][0], cols[ix == ix.max()][0]
914
-
915
- if timestamp < schedule[lowest].iat[0] or timestamp > schedule[highest].iat[-1]:
916
- raise ValueError("The provided timestamp is not covered by the schedule")
917
-
918
- day = schedule[schedule[lowest].le(timestamp)].iloc[-1].dropna().sort_values()
919
- day = day.loc[lowest:highest]
920
- day = day.index.to_series(index=day)
921
-
922
- if interrs.any():
923
- starts = day.str.startswith("interruption_start_")
924
- ends = day.str.startswith("interruption_end_")
925
- day.loc[starts] = False
926
- day.loc[ends] = True
927
-
928
- # When post follows market_close, market_close should not be considered a close
929
- day.loc[day.eq("market_close") & day.shift(-1).eq("post")] = "market_open"
930
- day = day.map(lambda x: (self.open_close_map.get(x) if x in self.open_close_map.keys() else x))
931
-
932
- if include_close:
933
- below = day.index < timestamp
934
- else:
935
- below = day.index <= timestamp
936
- return bool(day[below].iat[-1]) # returns numpy.bool_ if not bool(...)
937
-
938
- # need this to make is_open_now testable
939
- @staticmethod
940
- def _get_current_time():
941
- return pd.Timestamp.now(tz="UTC")
942
-
943
- def is_open_now(self, schedule, include_close=False, only_rth=False):
944
- """
945
- To determine if the current local system time (converted to UTC) is an open time for the market
946
-
947
- :param schedule: schedule DataFrame
948
- :param include_close: if False then the function will return False if the current local system time is equal to
949
- the closing timestamp. If True then it will return True if the current local system time is equal to the
950
- closing timestamp. Use True if using bars and would like to include the last bar as a valid open date
951
- and time.
952
- :param only_rth: whether to consider columns that are before market_open or after market_close
953
-
954
- :return: True if the current local system time is a valid open date and time, False if not
955
- """
956
- current_time = MarketCalendar._get_current_time()
957
- return self.open_at_time(schedule, current_time, include_close=include_close, only_rth=only_rth)
958
-
959
- def clean_dates(self, start_date, end_date):
960
- """
961
- Strips the inputs of time and time zone information
962
-
963
- :param start_date: start date
964
- :param end_date: end date
965
- :return: (start_date, end_date) with just date, no time and no time zone
966
- """
967
- start_date = pd.Timestamp(start_date).tz_localize(None).normalize()
968
- end_date = pd.Timestamp(end_date).tz_localize(None).normalize()
969
- return start_date, end_date
970
-
971
- def is_different(self, col, diff=None):
972
- if diff is None:
973
- diff = pd.Series.ne
974
- normal = self.days_at_time(col.index, col.name)
975
- return diff(col.dt.tz_convert("UTC"), normal)
976
-
977
- def early_closes(self, schedule):
978
- """
979
- Get a DataFrame of the dates that are an early close.
980
-
981
- :param schedule: schedule DataFrame
982
- :return: schedule DataFrame with rows that are early closes
983
- """
984
- return schedule[self.is_different(schedule["market_close"], pd.Series.lt)]
985
-
986
- def late_opens(self, schedule):
987
- """
988
- Get a DataFrame of the dates that are an late opens.
989
-
990
- :param schedule: schedule DataFrame
991
- :return: schedule DataFrame with rows that are late opens
992
- """
993
- return schedule[self.is_different(schedule["market_open"], pd.Series.gt)]
994
-
995
- def __getitem__(self, item):
996
- if isinstance(item, (tuple, list)):
997
- if item[1] == "all":
998
- return self.get_time(item[0], all_times=True)
999
- else:
1000
- return self.get_time_on(item[0], item[1])
1001
- else:
1002
- return self.get_time(item)
1003
-
1004
- def __setitem__(self, key, value):
1005
- return self.add_time(key, value)
1006
-
1007
- def __delitem__(self, key):
1008
- return self.remove_time(key)
1
+ # Fork of zipline from Quantopian. Licensed under MIT, original licence below
2
+ #
3
+ # Copyright 2016 Quantopian, Inc.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ import warnings
17
+ from abc import ABCMeta, abstractmethod
18
+ from datetime import time
19
+ from typing import Literal, Union
20
+
21
+ import pandas as pd
22
+ from pandas.tseries.offsets import CustomBusinessDay
23
+
24
+ from .class_registry import RegisteryMeta, ProtectedDict
25
+
26
+ from . import calendar_utils as u
27
+
28
+ MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY = range(7)
29
+
30
+ WEEKMASK_ABBR = {
31
+ MONDAY: "Mon",
32
+ TUESDAY: "Tue",
33
+ WEDNESDAY: "Wed",
34
+ THURSDAY: "Thu",
35
+ FRIDAY: "Fri",
36
+ SATURDAY: "Sat",
37
+ SUNDAY: "Sun",
38
+ }
39
+
40
+
41
+ class DEFAULT:
42
+ pass
43
+
44
+
45
+ class MarketCalendarMeta(ABCMeta, RegisteryMeta):
46
+ pass
47
+
48
+
49
+ class MarketCalendar(metaclass=MarketCalendarMeta):
50
+ """
51
+ An MarketCalendar represents the timing information of a single market or exchange.
52
+ Unless otherwise noted all times are in UTC and use Pandas data structures.
53
+ """
54
+
55
+ regular_market_times = {
56
+ "market_open": ((None, time(0)),),
57
+ "market_close": ((None, time(23)),),
58
+ }
59
+
60
+ open_close_map = {
61
+ "market_open": True,
62
+ "market_close": False,
63
+ "break_start": False,
64
+ "break_end": True,
65
+ "pre": True,
66
+ "post": False,
67
+ }
68
+
69
+ @staticmethod
70
+ def _tdelta(t, day_offset=0):
71
+ try:
72
+ return pd.Timedelta(
73
+ days=day_offset, hours=t.hour, minutes=t.minute, seconds=t.second
74
+ )
75
+ except AttributeError:
76
+ t, day_offset = t
77
+ return pd.Timedelta(
78
+ days=day_offset, hours=t.hour, minutes=t.minute, seconds=t.second
79
+ )
80
+
81
+ @staticmethod
82
+ def _off(tple):
83
+ try:
84
+ return tple[2]
85
+ except IndexError:
86
+ return 0
87
+
88
+ @classmethod
89
+ def calendar_names(cls):
90
+ """All Market Calendar names and aliases that can be used in "factory"
91
+ :return: list(str)
92
+ """
93
+ return [cal for cal in cls._regmeta_class_registry.keys() if cal not in ["MarketCalendar", "TradingCalendar"]]
94
+
95
+ @classmethod
96
+ def factory(cls, name, *args, **kwargs): # Will be set by Meta, keeping it there for tests
97
+ """
98
+ :param name: The name of the MarketCalendar to be retrieved.
99
+ :param *args/**kwargs: passed to requested MarketCalendar.__init__
100
+ :return: MarketCalendar of the desired calendar.
101
+ """
102
+ return
103
+
104
+ def __init__(self, open_time=None, close_time=None):
105
+ """
106
+ :param open_time: Market open time override as datetime.time object. If None then default is used.
107
+ :param close_time: Market close time override as datetime.time object. If None then default is used.
108
+ """
109
+
110
+ self.regular_market_times = self.regular_market_times.copy()
111
+ self.open_close_map = self.open_close_map.copy()
112
+ self._customized_market_times = []
113
+
114
+ if open_time is not None:
115
+ self.change_time("market_open", open_time)
116
+
117
+ if close_time is not None:
118
+ self.change_time("market_close", close_time)
119
+
120
+ if not hasattr(self, "_market_times"):
121
+ self._prepare_regular_market_times()
122
+
123
+ @property
124
+ @abstractmethod
125
+ def name(self):
126
+ """
127
+ Name of the market
128
+
129
+ :return: string name
130
+ """
131
+ raise NotImplementedError()
132
+
133
+ @property
134
+ def full_name(self):
135
+ """
136
+ Full name of the market
137
+
138
+ :return: string name
139
+ """
140
+ return self.name
141
+
142
+ @property
143
+ @abstractmethod
144
+ def tz(self):
145
+ """
146
+ Time zone for the market.
147
+
148
+ :return: timezone
149
+ """
150
+ raise NotImplementedError()
151
+
152
+ @property
153
+ def market_times(self):
154
+ return self._market_times
155
+
156
+ def _prepare_regular_market_times(self):
157
+ oc_map = self.open_close_map
158
+ assert all(isinstance(x, bool) for x in oc_map.values()), "Values in open_close_map need to be True or False"
159
+
160
+ regular = self.regular_market_times
161
+ discontinued = ProtectedDict()
162
+ regular_tds = {}
163
+
164
+ for market_time, times in regular.items():
165
+ # in case a market_time has been discontinued, extend the last time
166
+ # and add it to the discontinued_market_times dictionary
167
+ if market_time.startswith("interruption_"):
168
+ raise ValueError("'interruption_' prefix is reserved")
169
+
170
+ if times[-1][1] is None:
171
+ discontinued._set(market_time, times[-1][0])
172
+ times = times[:-1]
173
+ regular._set(market_time, times)
174
+
175
+ regular_tds[market_time] = tuple((t[0], self._tdelta(t[1], self._off(t))) for t in times)
176
+
177
+ if discontinued:
178
+ warnings.warn(
179
+ f"{list(discontinued.keys())} are discontinued, the dictionary"
180
+ f" `.discontinued_market_times` has the dates on which these were discontinued."
181
+ f" The times as of those dates are incorrect, use .remove_time(market_time)"
182
+ f" to ignore a market_time."
183
+ )
184
+
185
+ self.discontinued_market_times = discontinued
186
+ self.regular_market_times = regular
187
+
188
+ self._regular_market_timedeltas = regular_tds
189
+ self._market_times = sorted(regular.keys(), key=lambda x: regular_tds[x][-1][1])
190
+ self._oc_market_times = list(filter(oc_map.__contains__, self._market_times))
191
+
192
+ def _set_time(self, market_time, times, opens):
193
+ if isinstance(times, (tuple, list)): # passed a tuple
194
+ if not isinstance(times[0], (tuple, list)): # doesn't have a tuple inside
195
+ if times[0] is None: # seems to be a tuple indicating starting time
196
+ times = (times,)
197
+ else: # must be a tuple with: (time, offset)
198
+ times = ((None, times[0], times[1]),)
199
+ else: # should be a datetime.time object
200
+ times = ((None, times),)
201
+
202
+ ln = len(times)
203
+ for i, t in enumerate(times):
204
+ try:
205
+ assert t[0] is None or isinstance(t[0], str) or isinstance(t[0], pd.Timestamp)
206
+ assert isinstance(t[1], time) or (ln > 1 and i == ln - 1 and t[1] is None)
207
+ assert isinstance(self._off(t), int)
208
+ except AssertionError:
209
+ raise AssertionError(
210
+ "The passed time information is not in the right format, "
211
+ "please consult the docs for how to set market times"
212
+ )
213
+
214
+ if opens is DEFAULT:
215
+ opens = self.__class__.open_close_map.get(market_time, None)
216
+
217
+ if opens in (True, False):
218
+ self.open_close_map._set(market_time, opens)
219
+
220
+ elif opens is None: # make sure it's ignored
221
+ try:
222
+ self.open_close_map._del(market_time)
223
+ except KeyError:
224
+ pass
225
+ else:
226
+ raise ValueError("when you pass `opens`, it needs to be True, False, or None")
227
+
228
+ self.regular_market_times._set(market_time, times)
229
+
230
+ if not self.is_custom(market_time):
231
+ self._customized_market_times.append(market_time)
232
+
233
+ self._prepare_regular_market_times()
234
+
235
+ def change_time(self, market_time, times, opens=DEFAULT):
236
+ """
237
+ Changes the specified market time in regular_market_times and makes the necessary adjustments.
238
+
239
+ :param market_time: the market_time to change
240
+ :param times: new time information
241
+ :param opens: whether the market_time is a time that closes or opens the market
242
+ this is only needed if the market_time should be respected by .open_at_time
243
+ True: opens
244
+ False: closes
245
+ None: consider it neither opening nor closing, don't add to open_close_map (ignore in .open_at_time)
246
+ DEFAULT: same as None, unless the market_time is in self.__class__.open_close_map. Then it will take
247
+ the default value as defined by the class.
248
+ :return: None
249
+ """
250
+ assert market_time in self.regular_market_times, (
251
+ f"{market_time} is not in regular_market_times:" f"\n{self._market_times}."
252
+ )
253
+ return self._set_time(market_time, times, opens)
254
+
255
+ def add_time(self, market_time, times, opens=DEFAULT):
256
+ """
257
+ Adds the specified market time to regular_market_times and makes the necessary adjustments.
258
+
259
+ :param market_time: the market_time to add
260
+ :param times: the time information
261
+ :param opens: see .change_time docstring
262
+ :return: None
263
+ """
264
+ assert market_time not in self.regular_market_times, (
265
+ f"{market_time} is already in regular_market_times:" f"\n{self._market_times}"
266
+ )
267
+
268
+ return self._set_time(market_time, times, opens)
269
+
270
+ def remove_time(self, market_time):
271
+ """
272
+ Removes the specified market time from regular_market_times and makes the necessary adjustments.
273
+
274
+ :param market_time: the market_time to remove
275
+ :return: None
276
+ """
277
+
278
+ self.regular_market_times._del(market_time)
279
+ try:
280
+ self.open_close_map._del(market_time)
281
+ except KeyError:
282
+ pass
283
+
284
+ self._prepare_regular_market_times()
285
+ if self.is_custom(market_time):
286
+ self._customized_market_times.remove(market_time)
287
+
288
+ def is_custom(self, market_time):
289
+ return market_time in self._customized_market_times
290
+
291
+ @property
292
+ def has_custom(self):
293
+ return len(self._customized_market_times) > 0
294
+
295
+ def is_discontinued(self, market_time):
296
+ return market_time in self.discontinued_market_times
297
+
298
+ @property
299
+ def has_discontinued(self):
300
+ return len(self.discontinued_market_times) > 0
301
+
302
+ def get_time(self, market_time, all_times=False):
303
+ try:
304
+ times = self.regular_market_times[market_time]
305
+ except KeyError as e:
306
+ if "break_start" in market_time or "break_end" in market_time:
307
+ return None # in case of no breaks
308
+ elif market_time in ["market_open", "market_close"]:
309
+ raise NotImplementedError("You need to set market_times")
310
+ else:
311
+ raise e
312
+
313
+ if all_times:
314
+ return times
315
+ return times[-1][1].replace(tzinfo=self.tz)
316
+
317
+ def get_time_on(self, market_time, date):
318
+ times = self.get_time(market_time, all_times=True)
319
+ if times is None:
320
+ return None
321
+
322
+ date = pd.Timestamp(date)
323
+ for d, t in times[::-1]:
324
+ if d is None or pd.Timestamp(d) < date:
325
+ return t.replace(tzinfo=self.tz)
326
+
327
+ def open_time_on(self, date):
328
+ return self.get_time_on("market_open", date)
329
+
330
+ def close_time_on(self, date):
331
+ return self.get_time_on("market_close", date)
332
+
333
+ def break_start_on(self, date):
334
+ return self.get_time_on("break_start", date)
335
+
336
+ def break_end_on(self, date):
337
+ return self.get_time_on("break_end", date)
338
+
339
+ @property
340
+ def open_time(self):
341
+ """
342
+ Default open time for the market
343
+
344
+ :return: time
345
+ """
346
+ return self.get_time("market_open")
347
+
348
+ @property
349
+ def close_time(self):
350
+ """
351
+ Default close time for the market
352
+
353
+ :return: time
354
+ """
355
+ return self.get_time("market_close")
356
+
357
+ @property
358
+ def break_start(self):
359
+ """
360
+ Break time start. If None then there is no break
361
+
362
+ :return: time or None
363
+ """
364
+ return self.get_time("break_start")
365
+
366
+ @property
367
+ def break_end(self):
368
+ """
369
+ Break time end. If None then there is no break
370
+
371
+ :return: time or None
372
+ """
373
+ return self.get_time("break_end")
374
+
375
+ @property
376
+ def regular_holidays(self):
377
+ """
378
+
379
+ :return: pd.AbstractHolidayCalendar: a calendar containing the regular holidays for this calendar
380
+ """
381
+ return None
382
+
383
+ @property
384
+ def adhoc_holidays(self):
385
+ """
386
+
387
+ :return: list of ad-hoc holidays
388
+ """
389
+ return []
390
+
391
+ @property
392
+ def weekmask(self):
393
+ return "Mon Tue Wed Thu Fri"
394
+
395
+ @property
396
+ def special_opens(self):
397
+ """
398
+ A list of special open times and corresponding AbstractHolidayCalendar.
399
+
400
+ :return: List of (time, AbstractHolidayCalendar) tuples
401
+ """
402
+ return []
403
+
404
+ @property
405
+ def special_opens_adhoc(self):
406
+ """
407
+
408
+ :return: List of (time, DatetimeIndex) tuples that represent special opens that cannot be codified into rules.
409
+ """
410
+ return []
411
+
412
+ @property
413
+ def special_closes(self):
414
+ """
415
+ A list of special close times and corresponding HolidayCalendars.
416
+
417
+ :return: List of (time, AbstractHolidayCalendar) tuples
418
+ """
419
+ return []
420
+
421
+ @property
422
+ def special_closes_adhoc(self):
423
+ """
424
+
425
+ :return: List of (time, DatetimeIndex) tuples that represent special closes that cannot be codified into rules.
426
+ """
427
+ return []
428
+
429
+ def get_special_times(self, market_time):
430
+ return getattr(self, "special_" + market_time, [])
431
+
432
+ def get_special_times_adhoc(self, market_time):
433
+ return getattr(self, "special_" + market_time + "_adhoc", [])
434
+
435
+ def get_offset(self, market_time):
436
+ return self._off(self.get_time(market_time, all_times=True)[-1])
437
+
438
+ @property
439
+ def open_offset(self):
440
+ """
441
+ :return: open offset
442
+ """
443
+ return self.get_offset("market_open")
444
+
445
+ @property
446
+ def close_offset(self):
447
+ """
448
+ :return: close offset
449
+ """
450
+ return self.get_offset("market_close")
451
+
452
+ @property
453
+ def interruptions(self):
454
+ """
455
+ This needs to be a list with a tuple for each date that had an interruption.
456
+ The tuple should have this layout:
457
+
458
+ (date, start_time, end_time[, start_time2, end_time2, ...])
459
+
460
+ E.g.:
461
+ [
462
+ ("2002-02-03", (time(11), -1), time(11, 2)),
463
+ ("2010-01-11", time(11), (time(11, 1), 1)),
464
+ ("2010-01-13", time(9, 59), time(10), time(10, 29), time(10, 30)),
465
+ ("2011-01-10", time(11), time(11, 1))
466
+ ]
467
+
468
+ The date needs to be a string in this format: 'yyyy-mm-dd'.
469
+ Times need to be two datetime.time objects for each interruption, indicating start and end.
470
+ Optionally these can be wrapped in a tuple, where the
471
+ second element needs to be an integer indicating an offset.
472
+ On "2010-01-13" in the example, it is shown that there can be multiple interruptions in a day.
473
+ """
474
+ return []
475
+
476
+ def _convert(self, col: pd.Series):
477
+ """
478
+ col is a series indexed by dates at which interruptions occurred. The values are either the start or end times
479
+ of an interruption, represented by either a timedelta or a tuple with a timedelta and day offset of the form
480
+ (timedelta, offset). _convert produces a new series where the values are replaced by datetimes equal to the
481
+ index of the original series plus the offset if present, at the timedelta.
482
+
483
+ E.g.:
484
+ >>> self._convert(
485
+ pd.Series(
486
+ [datetime.time(11, 2), (datetime.time(11, 1), 1), datetime.time(10, 0), None],
487
+ index=pd.DatetimeIndex(['2002-02-03', '2010-01-11', '2010-01-13', '2011-01-10'])
488
+ )
489
+ )
490
+ 2002-02-03 2002-02-03 11:02:00+00:00
491
+ 2010-01-11 2010-01-12 11:01:00+00:00
492
+ 2010-01-13 2010-01-13 10:00:00+00:00
493
+ 2011-01-10 NaT
494
+ dtype: datetime64[ns, UTC]
495
+ """
496
+ col = col.dropna() # Python 3.8, pandas 2.0.3 cannot create time deltas from NaT
497
+ try:
498
+ times = col.str[0]
499
+ except AttributeError: # no tuples, only offset 0
500
+ return (
501
+ (pd.to_timedelta(col.astype("string").fillna(""), errors="coerce") + col.index)
502
+ .dt.tz_localize(self.tz)
503
+ .dt.tz_convert("UTC")
504
+ )
505
+
506
+ return (
507
+ (
508
+ pd.to_timedelta(times.fillna(col).astype("string").fillna(""), errors="coerce")
509
+ + pd.to_timedelta(col.str[1].fillna(0), unit="D")
510
+ + col.index
511
+ )
512
+ .dt.tz_localize(self.tz)
513
+ .dt.tz_convert("UTC")
514
+ )
515
+
516
+ @staticmethod
517
+ def _col_name(n: int):
518
+ return f"interruption_start_{n // 2 + 1}" if n % 2 == 1 else f"interruption_end_{n // 2}"
519
+
520
+ @property
521
+ def interruptions_df(self):
522
+ """
523
+ Will return a pd.DataFrame only containing interruptions.
524
+ """
525
+ if not self.interruptions:
526
+ return pd.DataFrame(index=pd.DatetimeIndex([]))
527
+ intr = pd.DataFrame(self.interruptions)
528
+ intr.index = pd.to_datetime(intr.pop(0))
529
+
530
+ intr.columns = map(self._col_name, intr.columns)
531
+ intr.index.name = None
532
+
533
+ return intr.apply(self._convert).sort_index()
534
+
535
+ def holidays(self) -> pd.tseries.offsets.CustomBusinessDay:
536
+ """
537
+ Returns the complete CustomBusinessDay object of holidays that can be used in any Pandas function that take
538
+ that input.
539
+
540
+ :return: CustomBusinessDay object of holidays
541
+ """
542
+ try:
543
+ return self._holidays
544
+ except AttributeError:
545
+ self._holidays = CustomBusinessDay(
546
+ holidays=self.adhoc_holidays,
547
+ calendar=self.regular_holidays,
548
+ weekmask=self.weekmask,
549
+ )
550
+ return self._holidays
551
+
552
+ def valid_days(self, start_date, end_date, tz="UTC") -> pd.DatetimeIndex:
553
+ """
554
+ Get a DatetimeIndex of valid open business days.
555
+
556
+ :param start_date: start date
557
+ :param end_date: end date
558
+ :param tz: time zone in either string or pytz.timezone
559
+ :return: DatetimeIndex of valid business days
560
+ """
561
+ return pd.date_range(start_date, end_date, freq=self.holidays(), normalize=True, tz=tz)
562
+
563
+ def _get_market_times(self, start, end):
564
+ mts = self._market_times
565
+ return mts[mts.index(start) : mts.index(end) + 1]
566
+
567
+ def days_at_time(self, days, market_time, day_offset=0):
568
+ """
569
+ Create an index of days at time ``t``, interpreted in timezone ``tz``. The returned index is localized to UTC.
570
+
571
+ In the example below, the times switch from 13:45 to 12:45 UTC because
572
+ March 13th is the daylight savings transition for US/Eastern. All the
573
+ times are still 8:45 when interpreted in US/Eastern.
574
+
575
+ >>> import pandas as pd; import datetime; import pprint
576
+ >>> dts = pd.date_range('2016-03-12', '2016-03-14')
577
+ >>> dts_at_845 = days_at_time(dts, datetime.time(8, 45), 'US/Eastern')
578
+ >>> pprint.pprint([str(dt) for dt in dts_at_845])
579
+ ['2016-03-12 13:45:00+00:00',
580
+ '2016-03-13 12:45:00+00:00',
581
+ '2016-03-14 12:45:00+00:00']
582
+
583
+ :param days: DatetimeIndex An index of dates (represented as midnight).
584
+ :param market_time: datetime.time The time to apply as an offset to each day in ``days``.
585
+ :param day_offset: int The number of days we want to offset @days by
586
+ :return: pd.Series of date with the time requested.
587
+ """
588
+ # Offset days without tz to avoid timezone issues.
589
+ days = pd.DatetimeIndex(days).tz_localize(None).to_series()
590
+
591
+ if isinstance(market_time, str): # if string, assume its a reference to saved market times
592
+ timedeltas = self._regular_market_timedeltas[market_time]
593
+ datetimes = days + timedeltas[0][1]
594
+ for cut_off, timedelta in timedeltas[1:]:
595
+ datetimes = datetimes.where(days < pd.Timestamp(cut_off), days + timedelta)
596
+
597
+ else: # otherwise, assume it is a datetime.time object
598
+ datetimes = days + self._tdelta(market_time, day_offset)
599
+
600
+ return datetimes.dt.tz_localize(self.tz).dt.tz_convert("UTC")
601
+
602
+ def _tryholidays(self, cal, s, e):
603
+ try:
604
+ # If the Calendar is all single Observance Holidays then it is far
605
+ # more efficient to extract and return those dates
606
+ observed_dates = u.all_single_observance_rules(cal)
607
+ if observed_dates is not None:
608
+ return pd.DatetimeIndex([date for date in observed_dates if s <= date <= e])
609
+ else:
610
+ return cal.holidays(s, e)
611
+ except ValueError:
612
+ return pd.DatetimeIndex([])
613
+
614
+ def _special_dates(self, calendars, ad_hoc_dates, start, end):
615
+ """
616
+ Union an iterable of pairs of the forms (time, calendar),
617
+ (time, [dates]), and (time, int). If the second item in the pair
618
+ is an int it will be interpreted as a specific day of the week.
619
+
620
+ (This is shared logic for computing special opens and special closes.)
621
+ """
622
+ indexes = []
623
+ for time_, calendar in calendars:
624
+ if isinstance(calendar, int):
625
+ day_of_week = CustomBusinessDay(weekmask=WEEKMASK_ABBR[calendar])
626
+ indexes.append(self.days_at_time(pd.date_range(start, end, freq=day_of_week), time_))
627
+ else:
628
+ indexes.append(self.days_at_time(self._tryholidays(calendar, start, end), time_))
629
+
630
+ indexes += [self.days_at_time(dates, time_) for time_, dates in ad_hoc_dates]
631
+
632
+ if indexes:
633
+ dates = pd.concat(indexes).sort_index().drop_duplicates()
634
+ return dates.loc[start : end.replace(hour=23, minute=59, second=59)]
635
+
636
+ return pd.Series([], dtype="datetime64[ns, UTC]", index=pd.DatetimeIndex([]))
637
+
638
+ def special_dates(self, market_time, start_date, end_date, filter_holidays=True):
639
+ """
640
+ Calculate a datetimeindex that only contains the specail times of the requested market time.
641
+
642
+ :param market_time: market_time reference
643
+ :param start_date: first possible date of the index
644
+ :param end_date: last possible date of the index
645
+ :param filter_holidays: will filter days by self.valid_days, which can be useful when debugging
646
+
647
+ :return: schedule DatetimeIndex
648
+ """
649
+ start_date, end_date = self.clean_dates(start_date, end_date)
650
+ calendars = self.get_special_times(market_time)
651
+ ad_hoc = self.get_special_times_adhoc(market_time)
652
+ special = self._special_dates(calendars, ad_hoc, start_date, end_date)
653
+
654
+ if filter_holidays:
655
+ valid = self.valid_days(start_date, end_date, tz=None)
656
+ special = special[special.index.isin(valid)] # some sources of special times don't exclude holidays
657
+ return special
658
+
659
+ def schedule(
660
+ self,
661
+ start_date,
662
+ end_date,
663
+ tz="UTC",
664
+ start="market_open",
665
+ end="market_close",
666
+ force_special_times=True,
667
+ market_times=None,
668
+ interruptions=False,
669
+ ) -> pd.DataFrame:
670
+ """
671
+ Generates the schedule DataFrame. The resulting DataFrame will have all the valid business days as the index
672
+ and columns for the requested market times. The columns can be determined either by setting a range (inclusive
673
+ on both sides), using `start` and `end`, or by passing a list to `market_times'. A range of market_times is
674
+ derived from a list of market_times that are available to the instance, which are sorted based on the current
675
+ regular time. See examples/usage.ipynb for demonstrations.
676
+
677
+ All time zones are set to UTC by default. Setting the tz parameter will convert the columns to the desired
678
+ timezone, such as 'America/New_York'.
679
+
680
+ :param start_date: first date of the schedule
681
+ :param end_date: last date of the schedule
682
+ :param tz: timezone that the columns of the returned schedule are in, default: "UTC"
683
+ :param start: the first market_time to include as a column, default: "market_open"
684
+ :param end: the last market_time to include as a column, default: "market_close"
685
+ :param force_special_times: how to handle special times.
686
+ True: overwrite regular times of the column itself, conform other columns to special times of
687
+ market_open/market_close if those are requested.
688
+ False: only overwrite regular times of the column itself, leave others alone
689
+ None: completely ignore special times
690
+ :param market_times: alternative to start/end, list of market_times that are in self.regular_market_times
691
+ :param interruptions: bool, whether to add interruptions to the schedule, default: False
692
+ These will be added as columns to the right of the DataFrame. Any interruption on a day between
693
+ start_date and end_date will be included, regardless of the market_times requested.
694
+ Also, `force_special_times` does not take these into consideration.
695
+ :return: schedule DataFrame
696
+ """
697
+ start_date, end_date = self.clean_dates(start_date, end_date)
698
+ if not (start_date <= end_date):
699
+ raise ValueError("start_date must be before or equal to end_date.")
700
+
701
+ _all_days = self.valid_days(start_date, end_date)
702
+
703
+ # Setup all valid trading days and the requested market_times
704
+ if market_times is None:
705
+ market_times = self._get_market_times(start, end)
706
+ elif market_times == "all":
707
+ market_times = self._market_times
708
+
709
+ if not _all_days.size: # If no valid days return an empty DataFrame
710
+ return pd.DataFrame(columns=market_times, index=pd.DatetimeIndex([], freq="C"))
711
+
712
+ return self.schedule_from_days(_all_days, tz, start, end, force_special_times, market_times, interruptions)
713
+
714
+ def schedule_from_days(
715
+ self,
716
+ days: pd.DatetimeIndex,
717
+ tz="UTC",
718
+ start="market_open",
719
+ end="market_close",
720
+ force_special_times=True,
721
+ market_times=None,
722
+ interruptions=False,
723
+ ) -> pd.DataFrame:
724
+ """
725
+ Generates a schedule DataFrame for the days provided. The days are assumed to be valid trading days.
726
+
727
+ The columns can be determined either by setting a range (inclusive on both sides), using `start` and `end`,
728
+ or by passing a list to `market_times'. A range of market_times is derived from a list of market_times that
729
+ are available to the instance, which are sorted based on the current regular time.
730
+ See examples/usage.ipynb for demonstrations.
731
+
732
+ All time zones are set to UTC by default. Setting the tz parameter will convert the columns to the desired
733
+ timezone, such as 'America/New_York'.
734
+
735
+ :param days: pd.DatetimeIndex of all the desired days in ascending order. This function does not double check
736
+ that these are valid trading days, it is assumed they are. It is intended that this parameter is generated
737
+ by either the .valid_days() or .date_range_htf() methods. Time & Timezone Information is ignored.
738
+ :param tz: timezone that the columns of the returned schedule are in, default: "UTC"
739
+ :param start: the first market_time to include as a column, default: "market_open"
740
+ :param end: the last market_time to include as a column, default: "market_close"
741
+ :param force_special_times: how to handle special times.
742
+ True: overwrite regular times of the column itself, conform other columns to special times of
743
+ market_open/market_close if those are requested.
744
+ False: only overwrite regular times of the column itself, leave others alone
745
+ None: completely ignore special times
746
+ :param market_times: alternative to start/end, list of market_times that are in self.regular_market_times
747
+ :param interruptions: bool, whether to add interruptions to the schedule, default: False
748
+ These will be added as columns to the right of the DataFrame. Any interruption on a day between
749
+ start_date and end_date will be included, regardless of the market_times requested.
750
+ Also, `force_special_times` does not take these into consideration.
751
+ :return: schedule DataFrame
752
+ """
753
+
754
+ if days.dtype != "datetime64[ns]":
755
+ days = pd.DatetimeIndex(days).normalize().tz_localize(None)
756
+
757
+ # Setup all valid trading days and the requested market_times
758
+ if market_times is None:
759
+ market_times = self._get_market_times(start, end)
760
+ elif market_times == "all":
761
+ market_times = self._market_times
762
+
763
+ _adj_others = force_special_times is True
764
+ _adj_col = force_special_times is not None
765
+ _open_adj = _close_adj = []
766
+
767
+ schedule = pd.DataFrame()
768
+ for market_time in market_times:
769
+ temp = self.days_at_time(days, market_time).copy() # standard times
770
+ if _adj_col:
771
+ # create an array of special times
772
+ special = self.special_dates(market_time, days[0], days[-1], filter_holidays=False)
773
+ # overwrite standard times
774
+ specialix = special.index[
775
+ special.index.isin(temp.index)
776
+ ] # some sources of special times don't exclude holidays
777
+ temp.loc[specialix] = special
778
+
779
+ if _adj_others:
780
+ if market_time == "market_open":
781
+ _open_adj = specialix
782
+ elif market_time == "market_close":
783
+ _close_adj = specialix
784
+
785
+ schedule[market_time] = temp
786
+
787
+ cols = schedule.columns
788
+ if _adj_others and len(_open_adj) > 0:
789
+ mkt_open_ind = cols.get_loc("market_open")
790
+
791
+ # Can't use Lambdas here since numpy array assignment doesn't return the array.
792
+ def adjust_opens(x): # x is an np.Array.
793
+ x[x <= x[mkt_open_ind]] = x[mkt_open_ind]
794
+ return x
795
+
796
+ adjusted = schedule.loc[_open_adj].apply(adjust_opens, axis=1, raw=True)
797
+ schedule.loc[_open_adj] = adjusted
798
+
799
+ if _adj_others and len(_close_adj) > 0:
800
+ mkt_close_ind = cols.get_loc("market_close")
801
+
802
+ def adjust_closes(x):
803
+ x[x >= x[mkt_close_ind]] = x[mkt_close_ind]
804
+ return x
805
+
806
+ adjusted = schedule.loc[_close_adj].apply(adjust_closes, axis=1, raw=True)
807
+ schedule.loc[_close_adj] = adjusted
808
+
809
+ if interruptions:
810
+ interrs = self.interruptions_df
811
+ schedule[interrs.columns] = interrs
812
+ schedule = schedule.dropna(how="all", axis=1)
813
+
814
+ if tz != "UTC":
815
+ schedule = schedule.apply(lambda s: s.dt.tz_convert(tz))
816
+
817
+ return schedule
818
+
819
+ def date_range_htf(
820
+ self,
821
+ frequency: Union[str, pd.Timedelta, int, float],
822
+ start: Union[str, pd.Timestamp, int, float, None] = None,
823
+ end: Union[str, pd.Timestamp, int, float, None] = None,
824
+ periods: Union[int, None] = None,
825
+ closed: Union[Literal["left", "right"], None] = "right",
826
+ *,
827
+ day_anchor: u.Day_Anchor = "SUN",
828
+ month_anchor: u.Month_Anchor = "JAN",
829
+ ) -> pd.DatetimeIndex:
830
+ """
831
+ Returns a Normalized DatetimeIndex from the start-date to End-Date for Time periods of 1D and Higher.
832
+
833
+ PARAMETERS:
834
+
835
+ :param frequency: String, Int/float (POSIX seconds) or pd.Timedelta of the desired frequency.
836
+ :Must be Greater than '1D' and an integer multiple of the base frequency (D, W, M, Q, or Y)
837
+ :Important Note: Ints/Floats & Timedeltas are always considered as 'Open Business Days',
838
+ '2D' == Every Other Buisness Day, '3D' == Every 3rd B.Day, '7D' == Every 7th B.Day
839
+ :Higher periods (passed as strings) align to the beginning or end of the relevant period
840
+ :i.e. '1W' == First/[Last] Trading Day of each Week, '1Q' == First/[Last] Day of every Quarter
841
+
842
+ :param start: String, Int/float (POSIX seconds) or pd.Timestamp of the desired start time.
843
+ :The Time & Timezone information is ignored. Only the Normalized Day is considered.
844
+
845
+ :param end: String, Int/float (POSIX seconds) or pd.Timestamp of the desired start time.
846
+ :The Time & Timezone information is ignored. Only the Normalized Day is considered.
847
+
848
+ :param periods: Optional Integer number of periods to return. If a Period count, Start time,
849
+ and End time are given the period count is ignored.
850
+
851
+ :param closed: Literal['left', 'right']. Method used to close each range.
852
+ :Left: First open trading day of the Session is returned (e.g. First Open Day of The Month)
853
+ :right: Last open trading day of the Session is returned (e.g. Last Open Day of The Month)
854
+ :Note, This has no effect when the desired frequency is a number of days.
855
+
856
+ :param day_anchor: Day to Anchor the start of the Weekly timeframes to. Default 'SUN'.
857
+ : To get the First/Last Days of the trading Week then the Anchor needs to be on a day the relevant
858
+ market is closed.
859
+ : This can be set so that a specific day each week is returned.
860
+ : freq='1W' & day_anchor='WED' Will return Every 'WED' when the market is open, and nearest day
861
+ to the left or right (based on 'closed') when the market is closed.
862
+ Options: ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"]
863
+
864
+ :param month_anchor: Month to Anchor the start of the year to for Quarter and yearly timeframes.
865
+ : Default 'JAN' for Calendar Quarters/Years. Can be set to 'JUL' to return Fiscal Years
866
+ Options: ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"]
867
+ """
868
+ return u.date_range_htf(
869
+ self.holidays(),
870
+ frequency,
871
+ start,
872
+ end,
873
+ periods,
874
+ closed,
875
+ day_anchor=day_anchor,
876
+ month_anchor=month_anchor,
877
+ )
878
+
879
+ def open_at_time(self, schedule, timestamp, include_close=False, only_rth=False):
880
+ """
881
+ Determine if a given timestamp is during an open time for the market. If the timestamp is
882
+ before the first open time or after the last close time of `schedule`, a ValueError will be raised.
883
+
884
+ :param schedule: schedule DataFrame
885
+ :param timestamp: the timestamp to check for. Assumed to be UTC, if it doesn't include tz information.
886
+ :param include_close: if False then the timestamp that equals the closing timestamp will return False and not be
887
+ considered a valid open date and time. If True then it will be considered valid and return True. Use True
888
+ if using bars and would like to include the last bar as a valid open date and time. The close refers to the
889
+ latest market_time available, which could be after market_close (e.g. 'post').
890
+ :param only_rth: whether to ignore columns that are before market_open or after market_close. If true,
891
+ include_close will be referring to market_close.
892
+ :return: True if the timestamp is a valid open date and time, False if not
893
+ """
894
+ timestamp = pd.Timestamp(timestamp)
895
+ try:
896
+ timestamp = timestamp.tz_localize("UTC")
897
+ except TypeError:
898
+ pass
899
+
900
+ cols = schedule.columns
901
+ interrs = cols.str.startswith("interruption_")
902
+ if not (cols.isin(self._oc_market_times) | interrs).all():
903
+ raise ValueError(
904
+ "You seem to be using a schedule that isn't based on the market_times, "
905
+ "or includes market_times that are not represented in the open_close_map."
906
+ )
907
+
908
+ if only_rth:
909
+ lowest, highest = "market_open", "market_close"
910
+ else:
911
+ cols = cols[~interrs]
912
+ ix = cols.map(self._oc_market_times.index)
913
+ lowest, highest = cols[ix == ix.min()][0], cols[ix == ix.max()][0]
914
+
915
+ if timestamp < schedule[lowest].iat[0] or timestamp > schedule[highest].iat[-1]:
916
+ raise ValueError("The provided timestamp is not covered by the schedule")
917
+
918
+ day = schedule[schedule[lowest].le(timestamp)].iloc[-1].dropna().sort_values()
919
+ day = day.loc[lowest:highest]
920
+ day = day.index.to_series(index=day)
921
+
922
+ if interrs.any():
923
+ starts = day.str.startswith("interruption_start_")
924
+ ends = day.str.startswith("interruption_end_")
925
+ day.loc[starts] = False
926
+ day.loc[ends] = True
927
+
928
+ # When post follows market_close, market_close should not be considered a close
929
+ day.loc[day.eq("market_close") & day.shift(-1).eq("post")] = "market_open"
930
+ day = day.map(lambda x: (self.open_close_map.get(x) if x in self.open_close_map.keys() else x))
931
+
932
+ if include_close:
933
+ below = day.index < timestamp
934
+ else:
935
+ below = day.index <= timestamp
936
+ return bool(day[below].iat[-1]) # returns numpy.bool_ if not bool(...)
937
+
938
+ # need this to make is_open_now testable
939
+ @staticmethod
940
+ def _get_current_time():
941
+ return pd.Timestamp.now(tz="UTC")
942
+
943
+ def is_open_now(self, schedule, include_close=False, only_rth=False):
944
+ """
945
+ To determine if the current local system time (converted to UTC) is an open time for the market
946
+
947
+ :param schedule: schedule DataFrame
948
+ :param include_close: if False then the function will return False if the current local system time is equal to
949
+ the closing timestamp. If True then it will return True if the current local system time is equal to the
950
+ closing timestamp. Use True if using bars and would like to include the last bar as a valid open date
951
+ and time.
952
+ :param only_rth: whether to consider columns that are before market_open or after market_close
953
+
954
+ :return: True if the current local system time is a valid open date and time, False if not
955
+ """
956
+ current_time = MarketCalendar._get_current_time()
957
+ return self.open_at_time(schedule, current_time, include_close=include_close, only_rth=only_rth)
958
+
959
+ def clean_dates(self, start_date, end_date):
960
+ """
961
+ Strips the inputs of time and time zone information
962
+
963
+ :param start_date: start date
964
+ :param end_date: end date
965
+ :return: (start_date, end_date) with just date, no time and no time zone
966
+ """
967
+ start_date = pd.Timestamp(start_date).tz_localize(None).normalize()
968
+ end_date = pd.Timestamp(end_date).tz_localize(None).normalize()
969
+ return start_date, end_date
970
+
971
+ def is_different(self, col, diff=None):
972
+ if diff is None:
973
+ diff = pd.Series.ne
974
+ normal = self.days_at_time(col.index, col.name)
975
+ return diff(col.dt.tz_convert("UTC"), normal)
976
+
977
+ def early_closes(self, schedule):
978
+ """
979
+ Get a DataFrame of the dates that are an early close.
980
+
981
+ :param schedule: schedule DataFrame
982
+ :return: schedule DataFrame with rows that are early closes
983
+ """
984
+ return schedule[self.is_different(schedule["market_close"], pd.Series.lt)]
985
+
986
+ def late_opens(self, schedule):
987
+ """
988
+ Get a DataFrame of the dates that are an late opens.
989
+
990
+ :param schedule: schedule DataFrame
991
+ :return: schedule DataFrame with rows that are late opens
992
+ """
993
+ return schedule[self.is_different(schedule["market_open"], pd.Series.gt)]
994
+
995
+ def __getitem__(self, item):
996
+ if isinstance(item, (tuple, list)):
997
+ if item[1] == "all":
998
+ return self.get_time(item[0], all_times=True)
999
+ else:
1000
+ return self.get_time_on(item[0], item[1])
1001
+ else:
1002
+ return self.get_time(item)
1003
+
1004
+ def __setitem__(self, key, value):
1005
+ return self.add_time(key, value)
1006
+
1007
+ def __delitem__(self, key):
1008
+ return self.remove_time(key)