pandas-market-calendars 4.3.3__py3-none-any.whl → 4.6.0__py3-none-any.whl

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