pandas-market-calendars 4.1.3__py3-none-any.whl → 4.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. {pandas_market_calendars-4.1.3.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.3.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 -119
  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.3.dist-info/RECORD +0 -45
  46. {pandas_market_calendars-4.1.3.dist-info → pandas_market_calendars-4.2.0.dist-info}/LICENSE +0 -0
  47. {pandas_market_calendars-4.1.3.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)