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.
- {pandas_market_calendars-4.1.3.dist-info → pandas_market_calendars-4.2.0.dist-info}/METADATA +200 -187
- pandas_market_calendars-4.2.0.dist-info/NOTICE +206 -0
- pandas_market_calendars-4.2.0.dist-info/RECORD +6 -0
- {pandas_market_calendars-4.1.3.dist-info → pandas_market_calendars-4.2.0.dist-info}/WHEEL +1 -1
- pandas_market_calendars/__init__.py +0 -37
- pandas_market_calendars/calendar_registry.py +0 -47
- pandas_market_calendars/calendar_utils.py +0 -225
- pandas_market_calendars/class_registry.py +0 -111
- pandas_market_calendars/exchange_calendar_asx.py +0 -63
- pandas_market_calendars/exchange_calendar_bmf.py +0 -227
- pandas_market_calendars/exchange_calendar_bse.py +0 -409
- pandas_market_calendars/exchange_calendar_cboe.py +0 -115
- pandas_market_calendars/exchange_calendar_cme.py +0 -240
- pandas_market_calendars/exchange_calendar_cme_globex_agriculture.py +0 -109
- pandas_market_calendars/exchange_calendar_cme_globex_base.py +0 -106
- pandas_market_calendars/exchange_calendar_cme_globex_energy_and_metals.py +0 -146
- pandas_market_calendars/exchange_calendar_cme_globex_equities.py +0 -104
- pandas_market_calendars/exchange_calendar_cme_globex_fixed_income.py +0 -114
- pandas_market_calendars/exchange_calendar_cme_globex_fx.py +0 -78
- pandas_market_calendars/exchange_calendar_eurex.py +0 -119
- pandas_market_calendars/exchange_calendar_hkex.py +0 -408
- pandas_market_calendars/exchange_calendar_ice.py +0 -65
- pandas_market_calendars/exchange_calendar_iex.py +0 -98
- pandas_market_calendars/exchange_calendar_jpx.py +0 -98
- pandas_market_calendars/exchange_calendar_lse.py +0 -91
- pandas_market_calendars/exchange_calendar_nyse.py +0 -1127
- pandas_market_calendars/exchange_calendar_ose.py +0 -150
- pandas_market_calendars/exchange_calendar_sifma.py +0 -300
- pandas_market_calendars/exchange_calendar_six.py +0 -114
- pandas_market_calendars/exchange_calendar_sse.py +0 -290
- pandas_market_calendars/exchange_calendar_tase.py +0 -119
- pandas_market_calendars/exchange_calendar_tsx.py +0 -159
- pandas_market_calendars/exchange_calendars_mirror.py +0 -114
- pandas_market_calendars/holidays_cme.py +0 -341
- pandas_market_calendars/holidays_cme_globex.py +0 -169
- pandas_market_calendars/holidays_cn.py +0 -1436
- pandas_market_calendars/holidays_jp.py +0 -362
- pandas_market_calendars/holidays_nyse.py +0 -1474
- pandas_market_calendars/holidays_oz.py +0 -65
- pandas_market_calendars/holidays_sifma.py +0 -321
- pandas_market_calendars/holidays_uk.py +0 -177
- pandas_market_calendars/holidays_us.py +0 -364
- pandas_market_calendars/jpx_equinox.py +0 -147
- pandas_market_calendars/market_calendar.py +0 -770
- pandas_market_calendars-4.1.3.dist-info/RECORD +0 -45
- {pandas_market_calendars-4.1.3.dist-info → pandas_market_calendars-4.2.0.dist-info}/LICENSE +0 -0
- {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)
|