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

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. pandas_market_calendars/__init__.py +38 -38
  2. pandas_market_calendars/calendar_registry.py +53 -52
  3. pandas_market_calendars/calendar_utils.py +261 -261
  4. pandas_market_calendars/calendars/asx.py +66 -66
  5. pandas_market_calendars/calendars/bmf.py +206 -206
  6. pandas_market_calendars/calendars/bse.py +407 -407
  7. pandas_market_calendars/calendars/cboe.py +145 -145
  8. pandas_market_calendars/calendars/cme.py +402 -402
  9. pandas_market_calendars/calendars/cme_globex_agriculture.py +126 -127
  10. pandas_market_calendars/calendars/cme_globex_base.py +119 -119
  11. pandas_market_calendars/calendars/cme_globex_crypto.py +160 -147
  12. pandas_market_calendars/calendars/cme_globex_energy_and_metals.py +216 -216
  13. pandas_market_calendars/calendars/cme_globex_equities.py +123 -121
  14. pandas_market_calendars/calendars/cme_globex_fixed_income.py +136 -134
  15. pandas_market_calendars/calendars/cme_globex_fx.py +101 -92
  16. pandas_market_calendars/calendars/eurex.py +139 -139
  17. pandas_market_calendars/calendars/eurex_fixed_income.py +98 -0
  18. pandas_market_calendars/calendars/hkex.py +426 -426
  19. pandas_market_calendars/calendars/ice.py +81 -81
  20. pandas_market_calendars/calendars/iex.py +112 -111
  21. pandas_market_calendars/calendars/jpx.py +109 -109
  22. pandas_market_calendars/calendars/lse.py +114 -114
  23. pandas_market_calendars/calendars/mirror.py +130 -129
  24. pandas_market_calendars/calendars/nyse.py +1324 -1324
  25. pandas_market_calendars/calendars/ose.py +116 -116
  26. pandas_market_calendars/calendars/sifma.py +350 -335
  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 +197 -195
  30. pandas_market_calendars/calendars/tsx.py +181 -181
  31. pandas_market_calendars/holidays/cme.py +385 -372
  32. pandas_market_calendars/holidays/cme_globex.py +214 -223
  33. pandas_market_calendars/holidays/cn.py +1455 -1455
  34. pandas_market_calendars/holidays/jp.py +398 -394
  35. pandas_market_calendars/holidays/nyse.py +1531 -1539
  36. pandas_market_calendars/holidays/oz.py +63 -65
  37. pandas_market_calendars/holidays/sifma.py +338 -350
  38. pandas_market_calendars/holidays/us.py +376 -377
  39. pandas_market_calendars/market_calendar.py +895 -895
  40. {pandas_market_calendars-4.3.2.dist-info → pandas_market_calendars-4.3.3.dist-info}/METADATA +3 -3
  41. pandas_market_calendars-4.3.3.dist-info/RECORD +50 -0
  42. pandas_market_calendars-4.3.2.dist-info/RECORD +0 -49
  43. {pandas_market_calendars-4.3.2.dist-info → pandas_market_calendars-4.3.3.dist-info}/LICENSE +0 -0
  44. {pandas_market_calendars-4.3.2.dist-info → pandas_market_calendars-4.3.3.dist-info}/NOTICE +0 -0
  45. {pandas_market_calendars-4.3.2.dist-info → pandas_market_calendars-4.3.3.dist-info}/WHEEL +0 -0
  46. {pandas_market_calendars-4.3.2.dist-info → pandas_market_calendars-4.3.3.dist-info}/top_level.txt +0 -0
@@ -1,895 +1,895 @@
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 not open_time is None:
108
- self.change_time("market_open", open_time)
109
-
110
- if not close_time is 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 not market_time 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 = not force_special_times is 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
+
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)