Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
wiseplat
GitHub Repository: wiseplat/python-code
Path: blob/master/ invest-robot-contest_TinkoffBotTwitch-main/venv/lib/python3.8/site-packages/pandas/tseries/holiday.py
7813 views
1
from __future__ import annotations
2
3
from datetime import (
4
datetime,
5
timedelta,
6
)
7
import warnings
8
9
from dateutil.relativedelta import ( # noqa:F401
10
FR,
11
MO,
12
SA,
13
SU,
14
TH,
15
TU,
16
WE,
17
)
18
import numpy as np
19
20
from pandas.errors import PerformanceWarning
21
22
from pandas import (
23
DateOffset,
24
DatetimeIndex,
25
Series,
26
Timestamp,
27
concat,
28
date_range,
29
)
30
31
from pandas.tseries.offsets import (
32
Day,
33
Easter,
34
)
35
36
37
def next_monday(dt: datetime) -> datetime:
38
"""
39
If holiday falls on Saturday, use following Monday instead;
40
if holiday falls on Sunday, use Monday instead
41
"""
42
if dt.weekday() == 5:
43
return dt + timedelta(2)
44
elif dt.weekday() == 6:
45
return dt + timedelta(1)
46
return dt
47
48
49
def next_monday_or_tuesday(dt: datetime) -> datetime:
50
"""
51
For second holiday of two adjacent ones!
52
If holiday falls on Saturday, use following Monday instead;
53
if holiday falls on Sunday or Monday, use following Tuesday instead
54
(because Monday is already taken by adjacent holiday on the day before)
55
"""
56
dow = dt.weekday()
57
if dow == 5 or dow == 6:
58
return dt + timedelta(2)
59
elif dow == 0:
60
return dt + timedelta(1)
61
return dt
62
63
64
def previous_friday(dt: datetime) -> datetime:
65
"""
66
If holiday falls on Saturday or Sunday, use previous Friday instead.
67
"""
68
if dt.weekday() == 5:
69
return dt - timedelta(1)
70
elif dt.weekday() == 6:
71
return dt - timedelta(2)
72
return dt
73
74
75
def sunday_to_monday(dt: datetime) -> datetime:
76
"""
77
If holiday falls on Sunday, use day thereafter (Monday) instead.
78
"""
79
if dt.weekday() == 6:
80
return dt + timedelta(1)
81
return dt
82
83
84
def weekend_to_monday(dt: datetime) -> datetime:
85
"""
86
If holiday falls on Sunday or Saturday,
87
use day thereafter (Monday) instead.
88
Needed for holidays such as Christmas observation in Europe
89
"""
90
if dt.weekday() == 6:
91
return dt + timedelta(1)
92
elif dt.weekday() == 5:
93
return dt + timedelta(2)
94
return dt
95
96
97
def nearest_workday(dt: datetime) -> datetime:
98
"""
99
If holiday falls on Saturday, use day before (Friday) instead;
100
if holiday falls on Sunday, use day thereafter (Monday) instead.
101
"""
102
if dt.weekday() == 5:
103
return dt - timedelta(1)
104
elif dt.weekday() == 6:
105
return dt + timedelta(1)
106
return dt
107
108
109
def next_workday(dt: datetime) -> datetime:
110
"""
111
returns next weekday used for observances
112
"""
113
dt += timedelta(days=1)
114
while dt.weekday() > 4:
115
# Mon-Fri are 0-4
116
dt += timedelta(days=1)
117
return dt
118
119
120
def previous_workday(dt: datetime) -> datetime:
121
"""
122
returns previous weekday used for observances
123
"""
124
dt -= timedelta(days=1)
125
while dt.weekday() > 4:
126
# Mon-Fri are 0-4
127
dt -= timedelta(days=1)
128
return dt
129
130
131
def before_nearest_workday(dt: datetime) -> datetime:
132
"""
133
returns previous workday after nearest workday
134
"""
135
return previous_workday(nearest_workday(dt))
136
137
138
def after_nearest_workday(dt: datetime) -> datetime:
139
"""
140
returns next workday after nearest workday
141
needed for Boxing day or multiple holidays in a series
142
"""
143
return next_workday(nearest_workday(dt))
144
145
146
class Holiday:
147
"""
148
Class that defines a holiday with start/end dates and rules
149
for observance.
150
"""
151
152
def __init__(
153
self,
154
name,
155
year=None,
156
month=None,
157
day=None,
158
offset=None,
159
observance=None,
160
start_date=None,
161
end_date=None,
162
days_of_week=None,
163
):
164
"""
165
Parameters
166
----------
167
name : str
168
Name of the holiday , defaults to class name
169
offset : array of pandas.tseries.offsets or
170
class from pandas.tseries.offsets
171
computes offset from date
172
observance: function
173
computes when holiday is given a pandas Timestamp
174
days_of_week:
175
provide a tuple of days e.g (0,1,2,3,) for Monday Through Thursday
176
Monday=0,..,Sunday=6
177
178
Examples
179
--------
180
>>> from pandas.tseries.holiday import Holiday, nearest_workday
181
>>> from dateutil.relativedelta import MO
182
183
>>> USMemorialDay = Holiday(
184
... "Memorial Day", month=5, day=31, offset=pd.DateOffset(weekday=MO(-1))
185
... )
186
>>> USMemorialDay
187
Holiday: Memorial Day (month=5, day=31, offset=<DateOffset: weekday=MO(-1)>)
188
189
>>> USLaborDay = Holiday(
190
... "Labor Day", month=9, day=1, offset=pd.DateOffset(weekday=MO(1))
191
... )
192
>>> USLaborDay
193
Holiday: Labor Day (month=9, day=1, offset=<DateOffset: weekday=MO(+1)>)
194
195
>>> July3rd = Holiday("July 3rd", month=7, day=3)
196
>>> July3rd
197
Holiday: July 3rd (month=7, day=3, )
198
199
>>> NewYears = Holiday(
200
... "New Years Day", month=1, day=1,
201
... observance=nearest_workday
202
... )
203
>>> NewYears # doctest: +SKIP
204
Holiday: New Years Day (
205
month=1, day=1, observance=<function nearest_workday at 0x66545e9bc440>
206
)
207
208
>>> July3rd = Holiday("July 3rd", month=7, day=3, days_of_week=(0, 1, 2, 3))
209
>>> July3rd
210
Holiday: July 3rd (month=7, day=3, )
211
"""
212
if offset is not None and observance is not None:
213
raise NotImplementedError("Cannot use both offset and observance.")
214
215
self.name = name
216
self.year = year
217
self.month = month
218
self.day = day
219
self.offset = offset
220
self.start_date = (
221
Timestamp(start_date) if start_date is not None else start_date
222
)
223
self.end_date = Timestamp(end_date) if end_date is not None else end_date
224
self.observance = observance
225
assert days_of_week is None or type(days_of_week) == tuple
226
self.days_of_week = days_of_week
227
228
def __repr__(self) -> str:
229
info = ""
230
if self.year is not None:
231
info += f"year={self.year}, "
232
info += f"month={self.month}, day={self.day}, "
233
234
if self.offset is not None:
235
info += f"offset={self.offset}"
236
237
if self.observance is not None:
238
info += f"observance={self.observance}"
239
240
repr = f"Holiday: {self.name} ({info})"
241
return repr
242
243
def dates(self, start_date, end_date, return_name=False):
244
"""
245
Calculate holidays observed between start date and end date
246
247
Parameters
248
----------
249
start_date : starting date, datetime-like, optional
250
end_date : ending date, datetime-like, optional
251
return_name : bool, optional, default=False
252
If True, return a series that has dates and holiday names.
253
False will only return dates.
254
"""
255
start_date = Timestamp(start_date)
256
end_date = Timestamp(end_date)
257
258
filter_start_date = start_date
259
filter_end_date = end_date
260
261
if self.year is not None:
262
dt = Timestamp(datetime(self.year, self.month, self.day))
263
if return_name:
264
return Series(self.name, index=[dt])
265
else:
266
return [dt]
267
268
dates = self._reference_dates(start_date, end_date)
269
holiday_dates = self._apply_rule(dates)
270
if self.days_of_week is not None:
271
holiday_dates = holiday_dates[
272
np.in1d(holiday_dates.dayofweek, self.days_of_week)
273
]
274
275
if self.start_date is not None:
276
filter_start_date = max(
277
self.start_date.tz_localize(filter_start_date.tz), filter_start_date
278
)
279
if self.end_date is not None:
280
filter_end_date = min(
281
self.end_date.tz_localize(filter_end_date.tz), filter_end_date
282
)
283
holiday_dates = holiday_dates[
284
(holiday_dates >= filter_start_date) & (holiday_dates <= filter_end_date)
285
]
286
if return_name:
287
return Series(self.name, index=holiday_dates)
288
return holiday_dates
289
290
def _reference_dates(self, start_date, end_date):
291
"""
292
Get reference dates for the holiday.
293
294
Return reference dates for the holiday also returning the year
295
prior to the start_date and year following the end_date. This ensures
296
that any offsets to be applied will yield the holidays within
297
the passed in dates.
298
"""
299
if self.start_date is not None:
300
start_date = self.start_date.tz_localize(start_date.tz)
301
302
if self.end_date is not None:
303
end_date = self.end_date.tz_localize(start_date.tz)
304
305
year_offset = DateOffset(years=1)
306
reference_start_date = Timestamp(
307
datetime(start_date.year - 1, self.month, self.day)
308
)
309
310
reference_end_date = Timestamp(
311
datetime(end_date.year + 1, self.month, self.day)
312
)
313
# Don't process unnecessary holidays
314
dates = date_range(
315
start=reference_start_date,
316
end=reference_end_date,
317
freq=year_offset,
318
tz=start_date.tz,
319
)
320
321
return dates
322
323
def _apply_rule(self, dates):
324
"""
325
Apply the given offset/observance to a DatetimeIndex of dates.
326
327
Parameters
328
----------
329
dates : DatetimeIndex
330
Dates to apply the given offset/observance rule
331
332
Returns
333
-------
334
Dates with rules applied
335
"""
336
if self.observance is not None:
337
return dates.map(lambda d: self.observance(d))
338
339
if self.offset is not None:
340
if not isinstance(self.offset, list):
341
offsets = [self.offset]
342
else:
343
offsets = self.offset
344
for offset in offsets:
345
346
# if we are adding a non-vectorized value
347
# ignore the PerformanceWarnings:
348
with warnings.catch_warnings():
349
warnings.simplefilter("ignore", PerformanceWarning)
350
dates += offset
351
return dates
352
353
354
holiday_calendars = {}
355
356
357
def register(cls):
358
try:
359
name = cls.name
360
except AttributeError:
361
name = cls.__name__
362
holiday_calendars[name] = cls
363
364
365
def get_calendar(name):
366
"""
367
Return an instance of a calendar based on its name.
368
369
Parameters
370
----------
371
name : str
372
Calendar name to return an instance of
373
"""
374
return holiday_calendars[name]()
375
376
377
class HolidayCalendarMetaClass(type):
378
def __new__(cls, clsname, bases, attrs):
379
calendar_class = super().__new__(cls, clsname, bases, attrs)
380
register(calendar_class)
381
return calendar_class
382
383
384
class AbstractHolidayCalendar(metaclass=HolidayCalendarMetaClass):
385
"""
386
Abstract interface to create holidays following certain rules.
387
"""
388
389
rules: list[Holiday] = []
390
start_date = Timestamp(datetime(1970, 1, 1))
391
end_date = Timestamp(datetime(2200, 12, 31))
392
_cache = None
393
394
def __init__(self, name=None, rules=None):
395
"""
396
Initializes holiday object with a given set a rules. Normally
397
classes just have the rules defined within them.
398
399
Parameters
400
----------
401
name : str
402
Name of the holiday calendar, defaults to class name
403
rules : array of Holiday objects
404
A set of rules used to create the holidays.
405
"""
406
super().__init__()
407
if name is None:
408
name = type(self).__name__
409
self.name = name
410
411
if rules is not None:
412
self.rules = rules
413
414
def rule_from_name(self, name):
415
for rule in self.rules:
416
if rule.name == name:
417
return rule
418
419
return None
420
421
def holidays(self, start=None, end=None, return_name=False):
422
"""
423
Returns a curve with holidays between start_date and end_date
424
425
Parameters
426
----------
427
start : starting date, datetime-like, optional
428
end : ending date, datetime-like, optional
429
return_name : bool, optional
430
If True, return a series that has dates and holiday names.
431
False will only return a DatetimeIndex of dates.
432
433
Returns
434
-------
435
DatetimeIndex of holidays
436
"""
437
if self.rules is None:
438
raise Exception(
439
f"Holiday Calendar {self.name} does not have any rules specified"
440
)
441
442
if start is None:
443
start = AbstractHolidayCalendar.start_date
444
445
if end is None:
446
end = AbstractHolidayCalendar.end_date
447
448
start = Timestamp(start)
449
end = Timestamp(end)
450
451
# If we don't have a cache or the dates are outside the prior cache, we
452
# get them again
453
if self._cache is None or start < self._cache[0] or end > self._cache[1]:
454
pre_holidays = [
455
rule.dates(start, end, return_name=True) for rule in self.rules
456
]
457
if pre_holidays:
458
holidays = concat(pre_holidays)
459
else:
460
holidays = Series(index=DatetimeIndex([]), dtype=object)
461
462
self._cache = (start, end, holidays.sort_index())
463
464
holidays = self._cache[2]
465
holidays = holidays[start:end]
466
467
if return_name:
468
return holidays
469
else:
470
return holidays.index
471
472
@staticmethod
473
def merge_class(base, other):
474
"""
475
Merge holiday calendars together. The base calendar
476
will take precedence to other. The merge will be done
477
based on each holiday's name.
478
479
Parameters
480
----------
481
base : AbstractHolidayCalendar
482
instance/subclass or array of Holiday objects
483
other : AbstractHolidayCalendar
484
instance/subclass or array of Holiday objects
485
"""
486
try:
487
other = other.rules
488
except AttributeError:
489
pass
490
491
if not isinstance(other, list):
492
other = [other]
493
other_holidays = {holiday.name: holiday for holiday in other}
494
495
try:
496
base = base.rules
497
except AttributeError:
498
pass
499
500
if not isinstance(base, list):
501
base = [base]
502
base_holidays = {holiday.name: holiday for holiday in base}
503
504
other_holidays.update(base_holidays)
505
return list(other_holidays.values())
506
507
def merge(self, other, inplace=False):
508
"""
509
Merge holiday calendars together. The caller's class
510
rules take precedence. The merge will be done
511
based on each holiday's name.
512
513
Parameters
514
----------
515
other : holiday calendar
516
inplace : bool (default=False)
517
If True set rule_table to holidays, else return array of Holidays
518
"""
519
holidays = self.merge_class(self, other)
520
if inplace:
521
self.rules = holidays
522
else:
523
return holidays
524
525
526
USMemorialDay = Holiday(
527
"Memorial Day", month=5, day=31, offset=DateOffset(weekday=MO(-1))
528
)
529
USLaborDay = Holiday("Labor Day", month=9, day=1, offset=DateOffset(weekday=MO(1)))
530
USColumbusDay = Holiday(
531
"Columbus Day", month=10, day=1, offset=DateOffset(weekday=MO(2))
532
)
533
USThanksgivingDay = Holiday(
534
"Thanksgiving Day", month=11, day=1, offset=DateOffset(weekday=TH(4))
535
)
536
USMartinLutherKingJr = Holiday(
537
"Birthday of Martin Luther King, Jr.",
538
start_date=datetime(1986, 1, 1),
539
month=1,
540
day=1,
541
offset=DateOffset(weekday=MO(3)),
542
)
543
USPresidentsDay = Holiday(
544
"Washington’s Birthday", month=2, day=1, offset=DateOffset(weekday=MO(3))
545
)
546
GoodFriday = Holiday("Good Friday", month=1, day=1, offset=[Easter(), Day(-2)])
547
548
EasterMonday = Holiday("Easter Monday", month=1, day=1, offset=[Easter(), Day(1)])
549
550
551
class USFederalHolidayCalendar(AbstractHolidayCalendar):
552
"""
553
US Federal Government Holiday Calendar based on rules specified by:
554
https://www.opm.gov/policy-data-oversight/
555
snow-dismissal-procedures/federal-holidays/
556
"""
557
558
rules = [
559
Holiday("New Year's Day", month=1, day=1, observance=nearest_workday),
560
USMartinLutherKingJr,
561
USPresidentsDay,
562
USMemorialDay,
563
Holiday(
564
"Juneteenth National Independence Day",
565
month=6,
566
day=19,
567
start_date="2021-06-18",
568
observance=nearest_workday,
569
),
570
Holiday("Independence Day", month=7, day=4, observance=nearest_workday),
571
USLaborDay,
572
USColumbusDay,
573
Holiday("Veterans Day", month=11, day=11, observance=nearest_workday),
574
USThanksgivingDay,
575
Holiday("Christmas Day", month=12, day=25, observance=nearest_workday),
576
]
577
578
579
def HolidayCalendarFactory(name, base, other, base_class=AbstractHolidayCalendar):
580
rules = AbstractHolidayCalendar.merge_class(base, other)
581
calendar_class = type(name, (base_class,), {"rules": rules, "name": name})
582
return calendar_class
583
584