pandas-market-calendars 4.1.4__py3-none-any.whl → 4.2.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-4.1.4.dist-info → pandas_market_calendars-4.2.0.dist-info}/METADATA +200 -187
  2. pandas_market_calendars-4.2.0.dist-info/NOTICE +206 -0
  3. pandas_market_calendars-4.2.0.dist-info/RECORD +6 -0
  4. {pandas_market_calendars-4.1.4.dist-info → pandas_market_calendars-4.2.0.dist-info}/WHEEL +1 -1
  5. pandas_market_calendars/__init__.py +0 -37
  6. pandas_market_calendars/calendar_registry.py +0 -47
  7. pandas_market_calendars/calendar_utils.py +0 -225
  8. pandas_market_calendars/class_registry.py +0 -111
  9. pandas_market_calendars/exchange_calendar_asx.py +0 -63
  10. pandas_market_calendars/exchange_calendar_bmf.py +0 -227
  11. pandas_market_calendars/exchange_calendar_bse.py +0 -409
  12. pandas_market_calendars/exchange_calendar_cboe.py +0 -115
  13. pandas_market_calendars/exchange_calendar_cme.py +0 -240
  14. pandas_market_calendars/exchange_calendar_cme_globex_agriculture.py +0 -109
  15. pandas_market_calendars/exchange_calendar_cme_globex_base.py +0 -106
  16. pandas_market_calendars/exchange_calendar_cme_globex_energy_and_metals.py +0 -146
  17. pandas_market_calendars/exchange_calendar_cme_globex_equities.py +0 -104
  18. pandas_market_calendars/exchange_calendar_cme_globex_fixed_income.py +0 -114
  19. pandas_market_calendars/exchange_calendar_cme_globex_fx.py +0 -78
  20. pandas_market_calendars/exchange_calendar_eurex.py +0 -119
  21. pandas_market_calendars/exchange_calendar_hkex.py +0 -408
  22. pandas_market_calendars/exchange_calendar_ice.py +0 -65
  23. pandas_market_calendars/exchange_calendar_iex.py +0 -98
  24. pandas_market_calendars/exchange_calendar_jpx.py +0 -98
  25. pandas_market_calendars/exchange_calendar_lse.py +0 -91
  26. pandas_market_calendars/exchange_calendar_nyse.py +0 -1127
  27. pandas_market_calendars/exchange_calendar_ose.py +0 -150
  28. pandas_market_calendars/exchange_calendar_sifma.py +0 -300
  29. pandas_market_calendars/exchange_calendar_six.py +0 -114
  30. pandas_market_calendars/exchange_calendar_sse.py +0 -290
  31. pandas_market_calendars/exchange_calendar_tase.py +0 -195
  32. pandas_market_calendars/exchange_calendar_tsx.py +0 -159
  33. pandas_market_calendars/exchange_calendars_mirror.py +0 -114
  34. pandas_market_calendars/holidays_cme.py +0 -341
  35. pandas_market_calendars/holidays_cme_globex.py +0 -169
  36. pandas_market_calendars/holidays_cn.py +0 -1436
  37. pandas_market_calendars/holidays_jp.py +0 -362
  38. pandas_market_calendars/holidays_nyse.py +0 -1474
  39. pandas_market_calendars/holidays_oz.py +0 -65
  40. pandas_market_calendars/holidays_sifma.py +0 -321
  41. pandas_market_calendars/holidays_uk.py +0 -177
  42. pandas_market_calendars/holidays_us.py +0 -364
  43. pandas_market_calendars/jpx_equinox.py +0 -147
  44. pandas_market_calendars/market_calendar.py +0 -770
  45. pandas_market_calendars-4.1.4.dist-info/RECORD +0 -45
  46. {pandas_market_calendars-4.1.4.dist-info → pandas_market_calendars-4.2.0.dist-info}/LICENSE +0 -0
  47. {pandas_market_calendars-4.1.4.dist-info → pandas_market_calendars-4.2.0.dist-info}/top_level.txt +0 -0
@@ -1,770 +0,0 @@
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
- class DEFAULT: pass
28
-
29
- class MarketCalendarMeta(ABCMeta, RegisteryMeta):
30
- pass
31
-
32
- class MarketCalendar(metaclass=MarketCalendarMeta):
33
- """
34
- An MarketCalendar represents the timing information of a single market or exchange.
35
- Unless otherwise noted all times are in UTC and use Pandas data structures.
36
- """
37
-
38
- regular_market_times = {"market_open": ((None, time(0)),),
39
- "market_close": ((None, time(23)),)
40
- }
41
-
42
- open_close_map = {"market_open": True,
43
- "market_close": False,
44
- "break_start": False,
45
- "break_end": True,
46
- "pre": True,
47
- "post": False}
48
-
49
- @staticmethod
50
- def _tdelta(t, day_offset= 0):
51
- try:
52
- return pd.Timedelta(days=day_offset, hours=t.hour, minutes=t.minute, seconds=t.second)
53
- except AttributeError:
54
- t, day_offset = t
55
- return pd.Timedelta(days=day_offset, hours=t.hour, minutes=t.minute, seconds=t.second)
56
-
57
- @staticmethod
58
- def _off(tple):
59
- try: return tple[2]
60
- except IndexError: return 0
61
-
62
- @classmethod
63
- def calendar_names(cls):
64
- """All Market Calendar names and aliases that can be used in "factory"
65
- :return: list(str)
66
- """
67
- return [cal for cal in cls._regmeta_class_registry.keys()
68
- if cal not in ['MarketCalendar', 'TradingCalendar']]
69
-
70
- @classmethod
71
- def factory(cls, name, *args, **kwargs): # Will be set by Meta, keeping it there for tests
72
- """
73
- :param name: The name of the MarketCalendar to be retrieved.
74
- :param *args/**kwargs: passed to requested MarketCalendar.__init__
75
- :return: MarketCalendar of the desired calendar.
76
- """
77
- return
78
-
79
- def __init__(self, open_time=None, close_time=None):
80
- """
81
- :param open_time: Market open time override as datetime.time object. If None then default is used.
82
- :param close_time: Market close time override as datetime.time object. If None then default is used.
83
- """
84
-
85
- self.regular_market_times = self.regular_market_times.copy()
86
- self.open_close_map = self.open_close_map.copy()
87
- self._customized_market_times = []
88
-
89
- if not open_time is None:
90
- self.change_time("market_open", open_time)
91
-
92
- if not close_time is None:
93
- self.change_time("market_close", close_time)
94
-
95
- if not hasattr(self, "_market_times"):
96
- self._prepare_regular_market_times()
97
-
98
- @property
99
- @abstractmethod
100
- def name(self):
101
- """
102
- Name of the market
103
-
104
- :return: string name
105
- """
106
- raise NotImplementedError()
107
-
108
- @property
109
- @abstractmethod
110
- def tz(self):
111
- """
112
- Time zone for the market.
113
-
114
- :return: timezone
115
- """
116
- raise NotImplementedError()
117
-
118
- @property
119
- def market_times(self):
120
- return self._market_times
121
-
122
- def _prepare_regular_market_times(self):
123
- oc_map = self.open_close_map
124
- assert all(isinstance(x, bool) for x in oc_map.values()), "Values in open_close_map need to be True or False"
125
-
126
- regular = self.regular_market_times
127
- discontinued = ProtectedDict()
128
- regular_tds = {}
129
-
130
- for market_time, times in regular.items():
131
- # in case a market_time has been discontinued, extend the last time
132
- # and add it to the discontinued_market_times dictionary
133
- if market_time.startswith("interruption_"):
134
- raise ValueError("'interruption_' prefix is reserved")
135
-
136
- if times[-1][1] is None:
137
- discontinued._set(market_time, times[-1][0])
138
- times = times[:-1]
139
- regular._set(market_time, times)
140
-
141
- regular_tds[market_time] = tuple((t[0], self._tdelta(t[1], self._off(t))) for t in times)
142
-
143
- if discontinued:
144
- warnings.warn(f"{list(discontinued.keys())} are discontinued, the dictionary"
145
- f" `.discontinued_market_times` has the dates on which these were discontinued."
146
- f" The times as of those dates are incorrect, use .remove_time(market_time)"
147
- f" to ignore a market_time.")
148
-
149
- self.discontinued_market_times = discontinued
150
- self.regular_market_times = regular
151
-
152
- self._regular_market_timedeltas = regular_tds
153
- self._market_times = sorted(regular.keys(), key= lambda x: regular_tds[x][-1][1])
154
- self._oc_market_times = list(filter(oc_map.__contains__, self._market_times))
155
-
156
- def _set_time(self, market_time, times, opens):
157
-
158
- if isinstance(times, (tuple, list)): # passed a tuple
159
- if not isinstance(times[0], (tuple, list)): # doesn't have a tuple inside
160
- if times[0] is None: # seems to be a tuple indicating starting time
161
- times = (times,)
162
- else: # must be a tuple with: (time, offset)
163
- times = ((None, times[0], times[1]),)
164
- else: # should be a datetime.time object
165
- times = ((None, times),)
166
-
167
- ln = len(times)
168
- for i, t in enumerate(times):
169
- try:
170
- assert t[0] is None or isinstance(t[0], str) or isinstance(t[0], pd.Timestamp)
171
- assert isinstance(t[1], time) or (ln > 1 and i == ln-1 and t[1] is None)
172
- assert isinstance(self._off(t), int)
173
- except AssertionError:
174
- raise AssertionError("The passed time information is not in the right format, "
175
- "please consult the docs for how to set market times")
176
-
177
- if opens is DEFAULT:
178
- opens = self.__class__.open_close_map.get(market_time, None)
179
-
180
- if opens in (True, False):
181
- self.open_close_map._set(market_time, opens)
182
-
183
- elif opens is None: # make sure it's ignored
184
- try: self.open_close_map._del(market_time)
185
- except KeyError: pass
186
- else:
187
- raise ValueError("when you pass `opens`, it needs to be True, False, or None")
188
-
189
- self.regular_market_times._set(market_time, times)
190
-
191
- if not self.is_custom(market_time):
192
- self._customized_market_times.append(market_time)
193
-
194
- self._prepare_regular_market_times()
195
-
196
-
197
- def change_time(self, market_time, times, opens= DEFAULT):
198
- """
199
- Changes the specified market time in regular_market_times and makes the necessary adjustments.
200
-
201
- :param market_time: the market_time to change
202
- :param times: new time information
203
- :param opens: whether the market_time is a time that closes or opens the market
204
- this is only needed if the market_time should be respected by .open_at_time
205
- True: opens
206
- False: closes
207
- None: consider it neither opening nor closing, don't add to open_close_map (ignore in .open_at_time)
208
- DEFAULT: same as None, unless the market_time is in self.__class__.open_close_map. Then it will take
209
- the default value as defined by the class.
210
- :return: None
211
- """
212
- assert market_time in self.regular_market_times, f"{market_time} is not in regular_market_times:" \
213
- f"\n{self._market_times}."
214
- return self._set_time(market_time, times, opens)
215
-
216
- def add_time(self, market_time, times, opens= DEFAULT):
217
- """
218
- Adds the specified market time to regular_market_times and makes the necessary adjustments.
219
-
220
- :param market_time: the market_time to add
221
- :param times: the time information
222
- :param opens: see .change_time docstring
223
- :return: None
224
- """
225
- assert not market_time in self.regular_market_times, f"{market_time} is already in regular_market_times:" \
226
- f"\n{self._market_times}"
227
-
228
- return self._set_time(market_time, times, opens)
229
-
230
- def remove_time(self, market_time):
231
- """
232
- Removes the specified market time from regular_market_times and makes the necessary adjustments.
233
-
234
- :param market_time: the market_time to remove
235
- :return: None
236
- """
237
-
238
- self.regular_market_times._del(market_time)
239
- try: self.open_close_map._del(market_time)
240
- except KeyError: pass
241
-
242
- self._prepare_regular_market_times()
243
- if self.is_custom(market_time):
244
- self._customized_market_times.remove(market_time)
245
-
246
- def is_custom(self, market_time):
247
- return market_time in self._customized_market_times
248
-
249
- @property
250
- def has_custom(self):
251
- return len(self._customized_market_times) > 0
252
-
253
- def is_discontinued(self, market_time):
254
- return market_time in self.discontinued_market_times
255
-
256
- @property
257
- def has_discontinued(self):
258
- return len(self.discontinued_market_times) > 0
259
-
260
- def get_time(self, market_time, all_times= False):
261
- try: times = self.regular_market_times[market_time]
262
- except KeyError as e:
263
- if "break_start" in market_time or "break_end" in market_time:
264
- return None # in case of no breaks
265
- elif market_time in ["market_open", "market_close"]:
266
- raise NotImplementedError("You need to set market_times")
267
- else:
268
- raise e
269
-
270
- if all_times: return times
271
- return times[-1][1].replace(tzinfo= self.tz)
272
-
273
- def get_time_on(self, market_time, date):
274
- times = self.get_time(market_time, all_times= True)
275
- if times is None: return None
276
-
277
- date = pd.Timestamp(date)
278
- for d, t in times[::-1]:
279
- if d is None or pd.Timestamp(d) < date:
280
- return t.replace(tzinfo= self.tz)
281
-
282
- def open_time_on(self, date): return self.get_time_on("market_open", date)
283
- def close_time_on(self, date): return self.get_time_on("market_close", date)
284
- def break_start_on(self, date): return self.get_time_on("break_start", date)
285
- def break_end_on(self, date): return self.get_time_on("break_end", date)
286
-
287
- @property
288
- def open_time(self):
289
- """
290
- Default open time for the market
291
-
292
- :return: time
293
- """
294
- return self.get_time("market_open")
295
-
296
- @property
297
- def close_time(self):
298
- """
299
- Default close time for the market
300
-
301
- :return: time
302
- """
303
- return self.get_time("market_close")
304
-
305
- @property
306
- def break_start(self):
307
- """
308
- Break time start. If None then there is no break
309
-
310
- :return: time or None
311
- """
312
- return self.get_time("break_start")
313
-
314
- @property
315
- def break_end(self):
316
- """
317
- Break time end. If None then there is no break
318
-
319
- :return: time or None
320
- """
321
- return self.get_time("break_end")
322
-
323
- @property
324
- def regular_holidays(self):
325
- """
326
-
327
- :return: pd.AbstractHolidayCalendar: a calendar containing the regular holidays for this calendar
328
- """
329
- return None
330
-
331
- @property
332
- def adhoc_holidays(self):
333
- """
334
-
335
- :return: list of ad-hoc holidays
336
- """
337
- return []
338
-
339
- @property
340
- def weekmask(self):
341
- return "Mon Tue Wed Thu Fri"
342
-
343
- @property
344
- def special_opens(self):
345
- """
346
- A list of special open times and corresponding AbstractHolidayCalendar.
347
-
348
- :return: List of (time, AbstractHolidayCalendar) tuples
349
- """
350
- return []
351
-
352
- @property
353
- def special_opens_adhoc(self):
354
- """
355
-
356
- :return: List of (time, DatetimeIndex) tuples that represent special opens that cannot be codified into rules.
357
- """
358
- return []
359
-
360
- @property
361
- def special_closes(self):
362
- """
363
- A list of special close times and corresponding HolidayCalendars.
364
-
365
- :return: List of (time, AbstractHolidayCalendar) tuples
366
- """
367
- return []
368
-
369
- @property
370
- def special_closes_adhoc(self):
371
- """
372
-
373
- :return: List of (time, DatetimeIndex) tuples that represent special closes that cannot be codified into rules.
374
- """
375
- return []
376
-
377
- def get_special_times(self, market_time):
378
- return getattr(self, "special_" + market_time, [])
379
-
380
- def get_special_times_adhoc(self, market_time):
381
- return getattr(self, "special_" + market_time + "_adhoc", [])
382
-
383
- def get_offset(self, market_time):
384
- return self._off(self.get_time(market_time, all_times= True)[-1])
385
-
386
- @property
387
- def open_offset(self):
388
- """
389
- :return: open offset
390
- """
391
- return self.get_offset("market_open")
392
-
393
- @property
394
- def close_offset(self):
395
- """
396
- :return: close offset
397
- """
398
- return self.get_offset("market_close")
399
-
400
- @property
401
- def interruptions(self):
402
- """
403
- This needs to be a list with a tuple for each date that had an interruption.
404
- The tuple should have this layout:
405
-
406
- (date, start_time, end_time[, start_time2, end_time2, ...])
407
-
408
- E.g.:
409
- [
410
- ("2002-02-03", (time(11), -1), time(11, 2)),
411
- ("2010-01-11", time(11), (time(11, 1), 1)),
412
- ("2010-01-13", time(9, 59), time(10), time(10, 29), time(10, 30)),
413
- ("2011-01-10", time(11), time(11, 1))
414
- ]
415
-
416
- The date needs to be a string in this format: 'yyyy-mm-dd'.
417
- Times need to be two datetime.time objects for each interruption, indicating start and end.
418
- Optionally these can be wrapped in a tuple, where the
419
- second element needs to be an integer indicating an offset.
420
- On "2010-01-13" in the example, it is shown that there can be multiple interruptions in a day.
421
- """
422
- return []
423
-
424
- def _convert(self, col):
425
- try: times = col.str[0]
426
- except AttributeError: # no tuples, only offset 0
427
- return (pd.to_timedelta(col.astype("string"), errors="coerce") + col.index
428
- ).dt.tz_localize(self.tz).dt.tz_convert("UTC")
429
-
430
- return (pd.to_timedelta(times.fillna(col).astype("string"), errors="coerce"
431
- ) + pd.to_timedelta(col.str[1].fillna(0), unit="D"
432
- ) + col.index
433
- ).dt.tz_localize(self.tz).dt.tz_convert("UTC")
434
-
435
- @property
436
- def interruptions_df(self):
437
- """
438
- Will return a pd.DataFrame only containing interruptions.
439
- """
440
- if not self.interruptions: return pd.DataFrame(index= pd.DatetimeIndex([]))
441
- intr = pd.DataFrame(self.interruptions)
442
- intr.index = pd.to_datetime(intr.pop(0))
443
-
444
- columns = []
445
- for i in range(1, intr.shape[1] // 2 + 1):
446
- i = str(i)
447
- columns.append("interruption_start_" + i)
448
- columns.append("interruption_end_" + i)
449
- intr.columns = columns
450
- intr.index.name = None
451
-
452
- return intr.apply(self._convert).sort_index()
453
-
454
- def holidays(self):
455
- """
456
- Returns the complete CustomBusinessDay object of holidays that can be used in any Pandas function that take
457
- that input.
458
-
459
- :return: CustomBusinessDay object of holidays
460
- """
461
- try: return self._holidays
462
- except AttributeError:
463
- self._holidays = CustomBusinessDay(
464
- holidays=self.adhoc_holidays,
465
- calendar=self.regular_holidays,
466
- weekmask=self.weekmask,
467
- )
468
- return self._holidays
469
-
470
- def valid_days(self, start_date, end_date, tz='UTC'):
471
- """
472
- Get a DatetimeIndex of valid open business days.
473
-
474
- :param start_date: start date
475
- :param end_date: end date
476
- :param tz: time zone in either string or pytz.timezone
477
- :return: DatetimeIndex of valid business days
478
- """
479
- return pd.date_range(start_date, end_date, freq=self.holidays(), normalize=True, tz=tz)
480
-
481
- def _get_market_times(self, start, end):
482
- mts = self._market_times
483
- return mts[mts.index(start): mts.index(end) + 1]
484
-
485
- def days_at_time(self, days, market_time, day_offset=0):
486
- """
487
- Create an index of days at time ``t``, interpreted in timezone ``tz``. The returned index is localized to UTC.
488
-
489
- In the example below, the times switch from 13:45 to 12:45 UTC because
490
- March 13th is the daylight savings transition for US/Eastern. All the
491
- times are still 8:45 when interpreted in US/Eastern.
492
-
493
- >>> import pandas as pd; import datetime; import pprint
494
- >>> dts = pd.date_range('2016-03-12', '2016-03-14')
495
- >>> dts_at_845 = days_at_time(dts, datetime.time(8, 45), 'US/Eastern')
496
- >>> pprint.pprint([str(dt) for dt in dts_at_845])
497
- ['2016-03-12 13:45:00+00:00',
498
- '2016-03-13 12:45:00+00:00',
499
- '2016-03-14 12:45:00+00:00']
500
-
501
- :param days: DatetimeIndex An index of dates (represented as midnight).
502
- :param market_time: datetime.time The time to apply as an offset to each day in ``days``.
503
- :param day_offset: int The number of days we want to offset @days by
504
- :return: pd.Series of date with the time requested.
505
- """
506
- # Offset days without tz to avoid timezone issues.
507
- days = pd.DatetimeIndex(days).tz_localize(None).to_series()
508
-
509
- if isinstance(market_time, str): # if string, assume its a reference to saved market times
510
- timedeltas = self._regular_market_timedeltas[market_time]
511
- datetimes = days + timedeltas[0][1]
512
- for cut_off, timedelta in timedeltas[1:]:
513
- datetimes = datetimes.where(days < pd.Timestamp(cut_off), days + timedelta)
514
-
515
- else: # otherwise, assume it is a datetime.time object
516
- datetimes = days + self._tdelta(market_time, day_offset)
517
-
518
- return datetimes.dt.tz_localize(self.tz).dt.tz_convert('UTC')
519
-
520
- def _tryholidays(self, cal, s, e):
521
- try: return cal.holidays(s, e)
522
- except ValueError: return pd.DatetimeIndex([])
523
-
524
- def _special_dates(self, calendars, ad_hoc_dates, start, end):
525
- """
526
- Union an iterable of pairs of the form (time, calendar)
527
- and an iterable of pairs of the form (time, [dates])
528
-
529
- (This is shared logic for computing special opens and special closes.)
530
- """
531
- indexes = [
532
- self.days_at_time(self._tryholidays(calendar, start, end), time_)
533
- for time_, calendar in calendars
534
- ] + [
535
- self.days_at_time(dates, time_) for time_, dates in ad_hoc_dates
536
- ]
537
- if indexes:
538
- dates = pd.concat(indexes).sort_index().drop_duplicates()
539
- return dates.loc[start: end.replace(hour=23, minute=59, second=59)]
540
-
541
- return pd.Series([], dtype= "datetime64[ns, UTC]", index= pd.DatetimeIndex([]))
542
-
543
- def special_dates(self, market_time, start_date, end_date, filter_holidays= True):
544
- """
545
- Calculate a datetimeindex that only contains the specail times of the requested market time.
546
-
547
- :param market_time: market_time reference
548
- :param start_date: first possible date of the index
549
- :param end_date: last possible date of the index
550
- :param filter_holidays: will filter days by self.valid_days, which can be useful when debugging
551
-
552
- :return: schedule DatetimeIndex
553
- """
554
- start_date, end_date = self.clean_dates(start_date, end_date)
555
- calendars = self.get_special_times(market_time)
556
- ad_hoc = self.get_special_times_adhoc(market_time)
557
- special = self._special_dates(calendars, ad_hoc, start_date, end_date)
558
-
559
- if filter_holidays:
560
- valid = self.valid_days(start_date, end_date, tz= None)
561
- special = special[special.index.isin(valid)] # some sources of special times don't exclude holidays
562
- return special
563
-
564
-
565
- def schedule(self, start_date, end_date, tz='UTC', start= "market_open", end= "market_close",
566
- force_special_times= True, market_times= None, interruptions= False):
567
- """
568
- Generates the schedule DataFrame. The resulting DataFrame will have all the valid business days as the index
569
- and columns for the requested market times. The columns can be determined either by setting a range (inclusive
570
- on both sides), using `start` and `end`, or by passing a list to `market_times'. A range of market_times is
571
- derived from a list of market_times that are available to the instance, which are sorted based on the current
572
- regular time. See examples/usage.ipynb for demonstrations.
573
-
574
- All time zones are set to UTC by default. Setting the tz parameter will convert the columns to the desired
575
- timezone, such as 'America/New_York'.
576
-
577
- :param start_date: first date of the schedule
578
- :param end_date: last date of the schedule
579
- :param tz: timezone that the columns of the returned schedule are in, default: "UTC"
580
- :param start: the first market_time to include as a column, default: "market_open"
581
- :param end: the last market_time to include as a column, default: "market_close"
582
- :param force_special_times: how to handle special times.
583
- True: overwrite regular times of the column itself, conform other columns to special times of
584
- market_open/market_close if those are requested.
585
- False: only overwrite regular times of the column itself, leave others alone
586
- None: completely ignore special times
587
- :param market_times: alternative to start/end, list of market_times that are in self.regular_market_times
588
- :param interruptions: bool, whether to add interruptions to the schedule, default: False
589
- These will be added as columns to the right of the DataFrame. Any interruption on a day between
590
- start_date and end_date will be included, regardless of the market_times requested.
591
- Also, `force_special_times` does not take these into consideration.
592
- :return: schedule DataFrame
593
- """
594
- start_date, end_date = self.clean_dates(start_date, end_date)
595
- if not (start_date <= end_date):
596
- raise ValueError('start_date must be before or equal to end_date.')
597
-
598
- # Setup all valid trading days and the requested market_times
599
- _all_days = self.valid_days(start_date, end_date)
600
- if market_times is None: market_times = self._get_market_times(start, end)
601
- elif market_times == "all": market_times = self._market_times
602
-
603
- # If no valid days return an empty DataFrame
604
- if not _all_days.size:
605
- return pd.DataFrame(columns=market_times, index=pd.DatetimeIndex([], freq='C'))
606
-
607
- _adj_others = force_special_times is True
608
- _adj_col = not force_special_times is None
609
- _open_adj = _close_adj = []
610
-
611
- schedule = pd.DataFrame()
612
- for market_time in market_times:
613
- temp = self.days_at_time(_all_days, market_time).copy() # standard times
614
- if _adj_col:
615
- # create an array of special times
616
- special = self.special_dates(market_time, start_date, end_date, filter_holidays= False)
617
- # overwrite standard times
618
- specialix = special.index[special.index.isin(temp.index)] # some sources of special times don't exclude holidays
619
- temp.loc[specialix] = special
620
-
621
- if _adj_others:
622
- if market_time == "market_open": _open_adj = specialix
623
- elif market_time == "market_close": _close_adj = specialix
624
-
625
- schedule[market_time] = temp
626
-
627
- if _adj_others:
628
- adjusted = schedule.loc[_open_adj].apply(
629
- lambda x: x.where(x.ge(x["market_open"]), x["market_open"]), axis= 1)
630
- schedule.loc[_open_adj] = adjusted
631
-
632
- adjusted = schedule.loc[_close_adj].apply(
633
- lambda x: x.where(x.le(x["market_close"]), x["market_close"]), axis= 1)
634
- schedule.loc[_close_adj] = adjusted
635
-
636
- if interruptions:
637
- interrs = self.interruptions_df
638
- schedule[interrs.columns] = interrs
639
- schedule = schedule.dropna(how= "all", axis= 1)
640
-
641
- if tz != "UTC":
642
- schedule = schedule.apply(lambda s: s.dt.tz_convert(tz))
643
-
644
- return schedule
645
-
646
-
647
- def open_at_time(self, schedule, timestamp, include_close=False, only_rth= False):
648
- """
649
- Determine if a given timestamp is during an open time for the market. If the timestamp is
650
- before the first open time or after the last close time of `schedule`, a ValueError will be raised.
651
-
652
- :param schedule: schedule DataFrame
653
- :param timestamp: the timestamp to check for. Assumed to be UTC, if it doesn't include tz information.
654
- :param include_close: if False then the timestamp that equals the closing timestamp will return False and not be
655
- considered a valid open date and time. If True then it will be considered valid and return True. Use True
656
- if using bars and would like to include the last bar as a valid open date and time. The close refers to the
657
- latest market_time available, which could be after market_close (e.g. 'post').
658
- :param only_rth: whether to ignore columns that are before market_open or after market_close. If true,
659
- include_close will be referring to market_close.
660
- :return: True if the timestamp is a valid open date and time, False if not
661
- """
662
- timestamp = pd.Timestamp(timestamp)
663
- try: timestamp = timestamp.tz_localize("UTC")
664
- except TypeError: pass
665
-
666
- cols = schedule.columns
667
- interrs = cols.str.startswith("interruption_")
668
- if not (cols.isin(self._oc_market_times) | interrs).all():
669
- raise ValueError("You seem to be using a schedule that isn't based on the market_times, "
670
- "or includes market_times that are not represented in the open_close_map.")
671
-
672
- if only_rth:
673
- lowest, highest = "market_open", "market_close"
674
- else:
675
- cols = cols[~interrs]
676
- ix = cols.map(self._oc_market_times.index)
677
- lowest, highest = cols[ix == ix.min()][0], cols[ix == ix.max()][0]
678
-
679
- if timestamp < schedule[lowest].iat[0] or timestamp > schedule[highest].iat[-1]:
680
- raise ValueError("The provided timestamp is not covered by the schedule")
681
-
682
- day = schedule[schedule[lowest].le(timestamp)].iloc[-1].dropna().sort_values()
683
- day = day.loc[lowest:highest]
684
- day = day.index.to_series(index= day)
685
-
686
- if interrs.any():
687
- starts = day.str.startswith("interruption_start_")
688
- ends = day.str.startswith("interruption_end_")
689
- day.loc[starts] = False
690
- day.loc[ends] = True
691
-
692
- # When post follows market_close, market_close should not be considered a close
693
- day.loc[day.eq("market_close") & day.shift(-1).eq("post")] = "market_open"
694
- day = day.replace(self.open_close_map)
695
-
696
- if include_close: below = day.index < timestamp
697
- else: below = day.index <= timestamp
698
- return bool(day[below].iat[-1]) # returns numpy.bool_ if not bool(...)
699
-
700
-
701
- # need this to make is_open_now testable
702
- @staticmethod
703
- def _get_current_time():
704
- return pd.Timestamp.now(tz='UTC')
705
-
706
- def is_open_now(self, schedule, include_close=False, only_rth=False):
707
- """
708
- To determine if the current local system time (converted to UTC) is an open time for the market
709
-
710
- :param schedule: schedule DataFrame
711
- :param include_close: if False then the function will return False if the current local system time is equal to
712
- the closing timestamp. If True then it will return True if the current local system time is equal to the
713
- closing timestamp. Use True if using bars and would like to include the last bar as a valid open date
714
- and time.
715
- :param only_rth: whether to consider columns that are before market_open or after market_close
716
-
717
- :return: True if the current local system time is a valid open date and time, False if not
718
- """
719
- current_time = MarketCalendar._get_current_time()
720
- return self.open_at_time(schedule, current_time, include_close=include_close, only_rth=only_rth)
721
-
722
- def clean_dates(self, start_date, end_date):
723
- """
724
- Strips the inputs of time and time zone information
725
-
726
- :param start_date: start date
727
- :param end_date: end date
728
- :return: (start_date, end_date) with just date, no time and no time zone
729
- """
730
- start_date = pd.Timestamp(start_date).tz_localize(None).normalize()
731
- end_date = pd.Timestamp(end_date).tz_localize(None).normalize()
732
- return start_date, end_date
733
-
734
- def is_different(self, col, diff= None):
735
- if diff is None: diff = pd.Series.ne
736
- normal = self.days_at_time(col.index, col.name)
737
- return diff(col.dt.tz_convert("UTC"), normal)
738
-
739
- def early_closes(self, schedule):
740
- """
741
- Get a DataFrame of the dates that are an early close.
742
-
743
- :param schedule: schedule DataFrame
744
- :return: schedule DataFrame with rows that are early closes
745
- """
746
- return schedule[self.is_different(schedule["market_close"], pd.Series.lt)]
747
-
748
- def late_opens(self, schedule):
749
- """
750
- Get a DataFrame of the dates that are an late opens.
751
-
752
- :param schedule: schedule DataFrame
753
- :return: schedule DataFrame with rows that are late opens
754
- """
755
- return schedule[self.is_different(schedule["market_open"], pd.Series.gt)]
756
-
757
- def __getitem__(self, item):
758
- if isinstance(item, (tuple, list)):
759
- if item[1] == "all":
760
- return self.get_time(item[0], all_times= True)
761
- else:
762
- return self.get_time_on(item[0], item[1])
763
- else:
764
- return self.get_time(item)
765
-
766
- def __setitem__(self, key, value):
767
- return self.add_time(key, value)
768
-
769
- def __delitem__(self, key):
770
- return self.remove_time(key)