opendate 0.1.34__cp39-cp39-win_amd64.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.
- date/__init__.py +3 -0
- opendate/__init__.py +150 -0
- opendate/_opendate.cp39-win_amd64.pyd +0 -0
- opendate/calendars.py +350 -0
- opendate/constants.py +58 -0
- opendate/date_.py +255 -0
- opendate/datetime_.py +339 -0
- opendate/decorators.py +198 -0
- opendate/extras.py +88 -0
- opendate/helpers.py +169 -0
- opendate/interval.py +408 -0
- opendate/metaclass.py +123 -0
- opendate/mixins/__init__.py +4 -0
- opendate/mixins/business.py +295 -0
- opendate/mixins/extras_.py +98 -0
- opendate/time_.py +139 -0
- opendate-0.1.34.dist-info/METADATA +469 -0
- opendate-0.1.34.dist-info/RECORD +20 -0
- opendate-0.1.34.dist-info/WHEEL +4 -0
- opendate-0.1.34.dist-info/licenses/LICENSE +23 -0
date/__init__.py
ADDED
opendate/__init__.py
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
__version__ = '0.1.34'
|
|
4
|
+
|
|
5
|
+
import datetime as _datetime
|
|
6
|
+
import zoneinfo as _zoneinfo
|
|
7
|
+
|
|
8
|
+
from opendate.calendars import Calendar, CustomCalendar, ExchangeCalendar
|
|
9
|
+
from opendate.calendars import available_calendars, get_calendar
|
|
10
|
+
from opendate.calendars import get_default_calendar, register_calendar
|
|
11
|
+
from opendate.calendars import set_default_calendar
|
|
12
|
+
from opendate.constants import EST, GMT, LCL, UTC, WEEKDAY_SHORTNAME, Timezone
|
|
13
|
+
from opendate.constants import WeekDay
|
|
14
|
+
from opendate.date_ import Date
|
|
15
|
+
from opendate.datetime_ import DateTime
|
|
16
|
+
from opendate.decorators import expect_date, expect_date_or_datetime
|
|
17
|
+
from opendate.decorators import expect_datetime, expect_native_timezone
|
|
18
|
+
from opendate.decorators import expect_time, expect_utc_timezone
|
|
19
|
+
from opendate.decorators import prefer_native_timezone, prefer_utc_timezone
|
|
20
|
+
from opendate.extras import create_ics, is_business_day
|
|
21
|
+
from opendate.extras import is_within_business_hours, overlap_days
|
|
22
|
+
from opendate.interval import Interval
|
|
23
|
+
from opendate.time_ import Time
|
|
24
|
+
|
|
25
|
+
timezone = Timezone
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def date(year: int, month: int, day: int) -> Date:
|
|
29
|
+
"""Create new Date
|
|
30
|
+
"""
|
|
31
|
+
return Date(year, month, day)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def datetime(
|
|
35
|
+
year: int,
|
|
36
|
+
month: int,
|
|
37
|
+
day: int,
|
|
38
|
+
hour: int = 0,
|
|
39
|
+
minute: int = 0,
|
|
40
|
+
second: int = 0,
|
|
41
|
+
microsecond: int = 0,
|
|
42
|
+
tzinfo: str | float | _zoneinfo.ZoneInfo | _datetime.tzinfo | None = UTC,
|
|
43
|
+
fold: int = 0,
|
|
44
|
+
) -> DateTime:
|
|
45
|
+
"""Create new DateTime
|
|
46
|
+
"""
|
|
47
|
+
return DateTime(
|
|
48
|
+
year,
|
|
49
|
+
month,
|
|
50
|
+
day,
|
|
51
|
+
hour=hour,
|
|
52
|
+
minute=minute,
|
|
53
|
+
second=second,
|
|
54
|
+
microsecond=microsecond,
|
|
55
|
+
tzinfo=tzinfo,
|
|
56
|
+
fold=fold,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def time(
|
|
61
|
+
hour: int,
|
|
62
|
+
minute: int = 0,
|
|
63
|
+
second: int = 0,
|
|
64
|
+
microsecond: int = 0,
|
|
65
|
+
tzinfo: str | float | _zoneinfo.ZoneInfo | _datetime.tzinfo | None = UTC,
|
|
66
|
+
) -> Time:
|
|
67
|
+
"""Create new Time
|
|
68
|
+
"""
|
|
69
|
+
return Time(hour, minute, second, microsecond, tzinfo)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def interval(begdate: Date | DateTime, enddate: Date | DateTime) -> Interval:
|
|
73
|
+
"""Create new Interval
|
|
74
|
+
"""
|
|
75
|
+
return Interval(begdate, enddate)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def parse(s: str | None, calendar: str | Calendar | None = None, raise_err: bool = False) -> DateTime | None:
|
|
79
|
+
"""Parse using DateTime.parse
|
|
80
|
+
"""
|
|
81
|
+
if calendar is None:
|
|
82
|
+
calendar = get_default_calendar()
|
|
83
|
+
return DateTime.parse(s, calendar=calendar, raise_err=raise_err)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def instance(obj: _datetime.date | _datetime.datetime | _datetime.time) -> DateTime | Date | Time:
|
|
87
|
+
"""Create a DateTime/Date/Time instance from a datetime/date/time native one.
|
|
88
|
+
"""
|
|
89
|
+
if isinstance(obj, _datetime.date) and not isinstance(obj, _datetime.datetime):
|
|
90
|
+
return Date.instance(obj)
|
|
91
|
+
if isinstance(obj, _datetime.time):
|
|
92
|
+
return Time.instance(obj)
|
|
93
|
+
if isinstance(obj, _datetime.datetime):
|
|
94
|
+
return DateTime.instance(obj)
|
|
95
|
+
raise ValueError(f'opendate `instance` helper cannot parse type {type(obj)}')
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def now(tz: str | _zoneinfo.ZoneInfo | None = None) -> DateTime:
|
|
99
|
+
"""Returns Datetime.now
|
|
100
|
+
"""
|
|
101
|
+
return DateTime.now(tz)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def today(tz: str | _zoneinfo.ZoneInfo | None = None) -> DateTime:
|
|
105
|
+
"""Returns DateTime.today
|
|
106
|
+
"""
|
|
107
|
+
return DateTime.today(tz)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
__all__ = [
|
|
111
|
+
'Date',
|
|
112
|
+
'date',
|
|
113
|
+
'DateTime',
|
|
114
|
+
'datetime',
|
|
115
|
+
'Calendar',
|
|
116
|
+
'ExchangeCalendar',
|
|
117
|
+
'CustomCalendar',
|
|
118
|
+
'get_calendar',
|
|
119
|
+
'get_default_calendar',
|
|
120
|
+
'set_default_calendar',
|
|
121
|
+
'available_calendars',
|
|
122
|
+
'register_calendar',
|
|
123
|
+
'expect_date',
|
|
124
|
+
'expect_datetime',
|
|
125
|
+
'expect_time',
|
|
126
|
+
'expect_date_or_datetime',
|
|
127
|
+
'expect_native_timezone',
|
|
128
|
+
'expect_utc_timezone',
|
|
129
|
+
'instance',
|
|
130
|
+
'Interval',
|
|
131
|
+
'interval',
|
|
132
|
+
'is_business_day',
|
|
133
|
+
'is_within_business_hours',
|
|
134
|
+
'LCL',
|
|
135
|
+
'now',
|
|
136
|
+
'overlap_days',
|
|
137
|
+
'parse',
|
|
138
|
+
'prefer_native_timezone',
|
|
139
|
+
'prefer_utc_timezone',
|
|
140
|
+
'Time',
|
|
141
|
+
'time',
|
|
142
|
+
'timezone',
|
|
143
|
+
'today',
|
|
144
|
+
'WeekDay',
|
|
145
|
+
'EST',
|
|
146
|
+
'GMT',
|
|
147
|
+
'UTC',
|
|
148
|
+
'WEEKDAY_SHORTNAME',
|
|
149
|
+
'create_ics',
|
|
150
|
+
]
|
|
Binary file
|
opendate/calendars.py
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import datetime as _datetime
|
|
4
|
+
import zoneinfo as _zoneinfo
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
|
|
7
|
+
import pandas as pd
|
|
8
|
+
import pandas_market_calendars as mcal
|
|
9
|
+
|
|
10
|
+
import opendate as _date
|
|
11
|
+
from opendate.constants import MAX_YEAR, UTC, Timezone
|
|
12
|
+
from opendate.helpers import _BusinessCalendar, _get_decade_bounds
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Calendar(ABC):
|
|
16
|
+
"""Abstract base class for calendar definitions.
|
|
17
|
+
|
|
18
|
+
Provides business day information including trading days,
|
|
19
|
+
market hours, and holidays. Use string-based calendars for
|
|
20
|
+
exchanges (via get_calendar()) or CustomCalendar for user-defined.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
name: str = 'calendar'
|
|
24
|
+
tz: _zoneinfo.ZoneInfo = UTC
|
|
25
|
+
|
|
26
|
+
@abstractmethod
|
|
27
|
+
def business_days(self, begdate: _datetime.date, enddate: _datetime.date) -> set:
|
|
28
|
+
"""Returns all business days over a range.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
@abstractmethod
|
|
32
|
+
def business_hours(self, begdate: _datetime.date, enddate: _datetime.date) -> dict:
|
|
33
|
+
"""Returns market open/close times for each business day.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def business_holidays(self, begdate: _datetime.date, enddate: _datetime.date) -> set:
|
|
38
|
+
"""Returns holidays over a range.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
@abstractmethod
|
|
42
|
+
def _get_calendar(self, date: _datetime.date):
|
|
43
|
+
"""Get Rust BusinessCalendar for O(1) operations.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ExchangeCalendar(Calendar):
|
|
48
|
+
"""Calendar backed by pandas_market_calendars.
|
|
49
|
+
|
|
50
|
+
Provides access to 150+ exchange calendars including NYSE, LSE,
|
|
51
|
+
NASDAQ, TSX, etc. Use get_calendar('NYSE') or available_calendars()
|
|
52
|
+
to discover options.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
BEGDATE = _datetime.date(2000, 1, 1)
|
|
56
|
+
ENDDATE = _datetime.date(2050, 1, 1)
|
|
57
|
+
|
|
58
|
+
def __init__(self, name: str):
|
|
59
|
+
self._name = name.upper()
|
|
60
|
+
self._mcal = mcal.get_calendar(self._name)
|
|
61
|
+
tz_str = str(self._mcal.tz)
|
|
62
|
+
self._tz = Timezone(tz_str)
|
|
63
|
+
self._business_days_cache: dict[tuple, set] = {}
|
|
64
|
+
self._business_hours_cache: dict[tuple, dict] = {}
|
|
65
|
+
self._business_holidays_cache: dict[tuple, set] = {}
|
|
66
|
+
self._fast_calendar_cache: dict[tuple, object] = {}
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def name(self) -> str:
|
|
70
|
+
return self._name
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def tz(self) -> _zoneinfo.ZoneInfo:
|
|
74
|
+
return self._tz
|
|
75
|
+
|
|
76
|
+
def business_days(self, begdate: _datetime.date = None, enddate: _datetime.date = None) -> set:
|
|
77
|
+
"""Get business days for a date range (loads and caches by decade).
|
|
78
|
+
"""
|
|
79
|
+
if begdate is None:
|
|
80
|
+
begdate = self.BEGDATE
|
|
81
|
+
if enddate is None:
|
|
82
|
+
enddate = self.ENDDATE
|
|
83
|
+
|
|
84
|
+
if begdate.year > MAX_YEAR:
|
|
85
|
+
return set()
|
|
86
|
+
|
|
87
|
+
decade_start = _datetime.date(begdate.year // 10 * 10, 1, 1)
|
|
88
|
+
next_decade_year = (enddate.year // 10 + 1) * 10
|
|
89
|
+
if next_decade_year > MAX_YEAR:
|
|
90
|
+
decade_end = _datetime.date(MAX_YEAR, 12, 31)
|
|
91
|
+
else:
|
|
92
|
+
decade_end = _datetime.date(next_decade_year, 1, 1)
|
|
93
|
+
|
|
94
|
+
return self._get_business_days_cached(decade_start, decade_end)
|
|
95
|
+
|
|
96
|
+
def _get_business_days_cached(self, begdate: _datetime.date, enddate: _datetime.date) -> set:
|
|
97
|
+
"""Internal method to load and cache business days by decade.
|
|
98
|
+
"""
|
|
99
|
+
key = (begdate, enddate)
|
|
100
|
+
if key not in self._business_days_cache:
|
|
101
|
+
self._business_days_cache[key] = {
|
|
102
|
+
_date.Date.instance(d.date())
|
|
103
|
+
for d in self._mcal.valid_days(begdate, enddate)
|
|
104
|
+
}
|
|
105
|
+
return self._business_days_cache[key]
|
|
106
|
+
|
|
107
|
+
def business_hours(self, begdate: _datetime.date = None, enddate: _datetime.date = None) -> dict:
|
|
108
|
+
"""Get market hours for a date range.
|
|
109
|
+
"""
|
|
110
|
+
if begdate is None:
|
|
111
|
+
begdate = self.BEGDATE
|
|
112
|
+
if enddate is None:
|
|
113
|
+
enddate = self.ENDDATE
|
|
114
|
+
|
|
115
|
+
key = (begdate, enddate)
|
|
116
|
+
if key not in self._business_hours_cache:
|
|
117
|
+
df = self._mcal.schedule(begdate, enddate, tz=self._tz)
|
|
118
|
+
open_close = [
|
|
119
|
+
(_date.DateTime.instance(o.to_pydatetime()),
|
|
120
|
+
_date.DateTime.instance(c.to_pydatetime()))
|
|
121
|
+
for o, c in zip(df.market_open, df.market_close)
|
|
122
|
+
]
|
|
123
|
+
self._business_hours_cache[key] = dict(zip(df.index.date, open_close))
|
|
124
|
+
return self._business_hours_cache[key]
|
|
125
|
+
|
|
126
|
+
def business_holidays(self, begdate: _datetime.date = None, enddate: _datetime.date = None) -> set:
|
|
127
|
+
"""Get business holidays for a date range.
|
|
128
|
+
"""
|
|
129
|
+
if begdate is None:
|
|
130
|
+
begdate = self.BEGDATE
|
|
131
|
+
if enddate is None:
|
|
132
|
+
enddate = self.ENDDATE
|
|
133
|
+
|
|
134
|
+
key = (begdate, enddate)
|
|
135
|
+
if key not in self._business_holidays_cache:
|
|
136
|
+
self._business_holidays_cache[key] = {
|
|
137
|
+
_date.Date.instance(d.date())
|
|
138
|
+
for d in map(pd.to_datetime, self._mcal.holidays().holidays)
|
|
139
|
+
if begdate <= d.date() <= enddate
|
|
140
|
+
}
|
|
141
|
+
return self._business_holidays_cache[key]
|
|
142
|
+
|
|
143
|
+
def _get_fast_calendar(self, decade_start: _datetime.date, decade_end: _datetime.date):
|
|
144
|
+
"""Get a BusinessCalendar for O(1) business day operations.
|
|
145
|
+
"""
|
|
146
|
+
key = (decade_start, decade_end)
|
|
147
|
+
if key not in self._fast_calendar_cache:
|
|
148
|
+
business_days = self._get_business_days_cached(decade_start, decade_end)
|
|
149
|
+
ordinals = sorted(d.toordinal() for d in business_days)
|
|
150
|
+
self._fast_calendar_cache[key] = _BusinessCalendar(ordinals)
|
|
151
|
+
return self._fast_calendar_cache[key]
|
|
152
|
+
|
|
153
|
+
def _get_calendar(self, date: _datetime.date):
|
|
154
|
+
"""Get the business calendar covering the decade containing the given date.
|
|
155
|
+
"""
|
|
156
|
+
bounds = _get_decade_bounds(date.year)
|
|
157
|
+
if bounds is None:
|
|
158
|
+
return None
|
|
159
|
+
return self._get_fast_calendar(*bounds)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class CustomCalendar(Calendar):
|
|
163
|
+
"""User-defined calendar with custom holidays and hours.
|
|
164
|
+
|
|
165
|
+
Example:
|
|
166
|
+
holidays = {Date(2024, 12, 26), Date(2024, 12, 27)}
|
|
167
|
+
cal = CustomCalendar(
|
|
168
|
+
name='MyCompany',
|
|
169
|
+
holidays=holidays,
|
|
170
|
+
tz=Timezone('US/Eastern'),
|
|
171
|
+
)
|
|
172
|
+
d = Date(2024, 12, 25).calendar(cal).b.add(days=1)
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
def __init__(
|
|
176
|
+
self,
|
|
177
|
+
name: str = 'custom',
|
|
178
|
+
holidays: set[_datetime.date] | callable = None,
|
|
179
|
+
tz: _zoneinfo.ZoneInfo = UTC,
|
|
180
|
+
weekmask: str = 'Mon Tue Wed Thu Fri',
|
|
181
|
+
open_time: _datetime.time = _datetime.time(9, 30),
|
|
182
|
+
close_time: _datetime.time = _datetime.time(16, 0),
|
|
183
|
+
):
|
|
184
|
+
self._name = name
|
|
185
|
+
self._holidays = holidays or set()
|
|
186
|
+
self._tz = tz
|
|
187
|
+
self._weekmask = weekmask
|
|
188
|
+
self._open_time = open_time
|
|
189
|
+
self._close_time = close_time
|
|
190
|
+
self._weekday_set = self._parse_weekmask(weekmask)
|
|
191
|
+
self._fast_calendar_cache: dict[tuple, object] = {}
|
|
192
|
+
|
|
193
|
+
def _parse_weekmask(self, weekmask: str) -> set[int]:
|
|
194
|
+
"""Parse weekmask string into set of weekday numbers (0=Mon, 6=Sun).
|
|
195
|
+
"""
|
|
196
|
+
day_map = {
|
|
197
|
+
'mon': 0, 'tue': 1, 'wed': 2, 'thu': 3,
|
|
198
|
+
'fri': 4, 'sat': 5, 'sun': 6,
|
|
199
|
+
}
|
|
200
|
+
return {day_map[d.lower()] for d in weekmask.split() if d.lower() in day_map}
|
|
201
|
+
|
|
202
|
+
@property
|
|
203
|
+
def name(self) -> str:
|
|
204
|
+
return self._name
|
|
205
|
+
|
|
206
|
+
@property
|
|
207
|
+
def tz(self) -> _zoneinfo.ZoneInfo:
|
|
208
|
+
return self._tz
|
|
209
|
+
|
|
210
|
+
def _get_holidays(self, begdate: _datetime.date, enddate: _datetime.date) -> set:
|
|
211
|
+
"""Get holidays for the date range.
|
|
212
|
+
"""
|
|
213
|
+
if callable(self._holidays):
|
|
214
|
+
return self._holidays(begdate, enddate)
|
|
215
|
+
return {h for h in self._holidays if begdate <= h <= enddate}
|
|
216
|
+
|
|
217
|
+
def business_days(self, begdate: _datetime.date = None, enddate: _datetime.date = None) -> set:
|
|
218
|
+
"""Get business days for a date range.
|
|
219
|
+
"""
|
|
220
|
+
if begdate is None:
|
|
221
|
+
begdate = _datetime.date(2000, 1, 1)
|
|
222
|
+
if enddate is None:
|
|
223
|
+
enddate = _datetime.date(2050, 1, 1)
|
|
224
|
+
|
|
225
|
+
holidays = self._get_holidays(begdate, enddate)
|
|
226
|
+
result = set()
|
|
227
|
+
current = begdate
|
|
228
|
+
while current <= enddate:
|
|
229
|
+
if current.weekday() in self._weekday_set and current not in holidays:
|
|
230
|
+
result.add(_date.Date.instance(current))
|
|
231
|
+
current += _datetime.timedelta(days=1)
|
|
232
|
+
return result
|
|
233
|
+
|
|
234
|
+
def business_hours(self, begdate: _datetime.date = None, enddate: _datetime.date = None) -> dict:
|
|
235
|
+
"""Get market hours for a date range.
|
|
236
|
+
"""
|
|
237
|
+
business_days = self.business_days(begdate, enddate)
|
|
238
|
+
result = {}
|
|
239
|
+
for d in business_days:
|
|
240
|
+
open_dt = _date.DateTime(
|
|
241
|
+
d.year, d.month, d.day,
|
|
242
|
+
self._open_time.hour, self._open_time.minute, self._open_time.second,
|
|
243
|
+
tzinfo=self._tz,
|
|
244
|
+
)
|
|
245
|
+
close_dt = _date.DateTime(
|
|
246
|
+
d.year, d.month, d.day,
|
|
247
|
+
self._close_time.hour, self._close_time.minute, self._close_time.second,
|
|
248
|
+
tzinfo=self._tz,
|
|
249
|
+
)
|
|
250
|
+
result[d] = (open_dt, close_dt)
|
|
251
|
+
return result
|
|
252
|
+
|
|
253
|
+
def business_holidays(self, begdate: _datetime.date = None, enddate: _datetime.date = None) -> set:
|
|
254
|
+
"""Get business holidays for a date range.
|
|
255
|
+
"""
|
|
256
|
+
if begdate is None:
|
|
257
|
+
begdate = _datetime.date(2000, 1, 1)
|
|
258
|
+
if enddate is None:
|
|
259
|
+
enddate = _datetime.date(2050, 1, 1)
|
|
260
|
+
return {_date.Date.instance(h) if not isinstance(h, _date.Date) else h
|
|
261
|
+
for h in self._get_holidays(begdate, enddate)}
|
|
262
|
+
|
|
263
|
+
def _get_calendar(self, date: _datetime.date):
|
|
264
|
+
"""Get the business calendar for O(1) operations.
|
|
265
|
+
"""
|
|
266
|
+
bounds = _get_decade_bounds(date.year)
|
|
267
|
+
if bounds is None:
|
|
268
|
+
return None
|
|
269
|
+
decade_start, decade_end = bounds
|
|
270
|
+
key = (decade_start, decade_end)
|
|
271
|
+
if key not in self._fast_calendar_cache:
|
|
272
|
+
business_days = self.business_days(decade_start, decade_end)
|
|
273
|
+
ordinals = sorted(d.toordinal() for d in business_days)
|
|
274
|
+
self._fast_calendar_cache[key] = _BusinessCalendar(ordinals)
|
|
275
|
+
return self._fast_calendar_cache[key]
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
_calendar_cache: dict[str, Calendar] = {}
|
|
279
|
+
_default_calendar: str = 'NYSE'
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def get_default_calendar() -> str:
|
|
283
|
+
"""Get the default calendar name used when no calendar is specified.
|
|
284
|
+
|
|
285
|
+
Returns
|
|
286
|
+
Current default calendar name (initially 'NYSE')
|
|
287
|
+
"""
|
|
288
|
+
return _default_calendar
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def set_default_calendar(name: str) -> None:
|
|
292
|
+
"""Set the default calendar used when no calendar is specified.
|
|
293
|
+
|
|
294
|
+
Parameters
|
|
295
|
+
name: Calendar name (e.g., 'NYSE', 'LSE', or a registered custom name)
|
|
296
|
+
|
|
297
|
+
Raises
|
|
298
|
+
ValueError: If calendar name is not recognized
|
|
299
|
+
"""
|
|
300
|
+
global _default_calendar
|
|
301
|
+
get_calendar(name)
|
|
302
|
+
_default_calendar = name.upper()
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def get_calendar(name: str) -> Calendar:
|
|
306
|
+
"""Get or create a calendar instance by name.
|
|
307
|
+
|
|
308
|
+
Parameters
|
|
309
|
+
name: Exchange name (e.g., 'NYSE', 'LSE') or registered custom name
|
|
310
|
+
|
|
311
|
+
Returns
|
|
312
|
+
Calendar instance
|
|
313
|
+
|
|
314
|
+
Raises
|
|
315
|
+
ValueError: If calendar name is not recognized
|
|
316
|
+
"""
|
|
317
|
+
name_upper = name.upper()
|
|
318
|
+
|
|
319
|
+
if name_upper in _calendar_cache:
|
|
320
|
+
return _calendar_cache[name_upper]
|
|
321
|
+
|
|
322
|
+
valid_names = set(mcal.get_calendar_names())
|
|
323
|
+
if name_upper in valid_names or name in valid_names:
|
|
324
|
+
cal = ExchangeCalendar(name)
|
|
325
|
+
_calendar_cache[name_upper] = cal
|
|
326
|
+
return cal
|
|
327
|
+
|
|
328
|
+
raise ValueError(
|
|
329
|
+
f'Unknown calendar: {name}. '
|
|
330
|
+
f'Use available_calendars() to see valid options.'
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def available_calendars() -> list[str]:
|
|
335
|
+
"""List all available exchange calendar names.
|
|
336
|
+
|
|
337
|
+
Returns
|
|
338
|
+
Sorted list of calendar names (e.g., ['NYSE', 'LSE', 'NASDAQ', ...])
|
|
339
|
+
"""
|
|
340
|
+
return sorted(mcal.get_calendar_names())
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def register_calendar(name: str, calendar: Calendar) -> None:
|
|
344
|
+
"""Register a custom calendar for use by name.
|
|
345
|
+
|
|
346
|
+
Parameters
|
|
347
|
+
name: Name to register the calendar under
|
|
348
|
+
calendar: Calendar instance
|
|
349
|
+
"""
|
|
350
|
+
_calendar_cache[name.upper()] = calendar
|
opendate/constants.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import zoneinfo as _zoneinfo
|
|
6
|
+
|
|
7
|
+
import pendulum as _pendulum
|
|
8
|
+
|
|
9
|
+
_IS_WINDOWS = os.name == 'nt'
|
|
10
|
+
|
|
11
|
+
MIN_YEAR = 1900
|
|
12
|
+
MAX_YEAR = 2100
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def Timezone(name: str = 'US/Eastern') -> _zoneinfo.ZoneInfo:
|
|
16
|
+
"""Create a timezone object with the specified name.
|
|
17
|
+
|
|
18
|
+
Simple wrapper around Pendulum's Timezone function that ensures
|
|
19
|
+
consistent timezone handling across the library. Note that 'US/Eastern'
|
|
20
|
+
is equivalent to 'America/New_York' for all dates.
|
|
21
|
+
"""
|
|
22
|
+
return _pendulum.tz.Timezone(name)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
UTC = Timezone('UTC')
|
|
26
|
+
GMT = Timezone('GMT')
|
|
27
|
+
EST = Timezone('US/Eastern')
|
|
28
|
+
LCL = _pendulum.tz.Timezone(_pendulum.tz.get_local_timezone().name)
|
|
29
|
+
|
|
30
|
+
WeekDay = _pendulum.day.WeekDay
|
|
31
|
+
|
|
32
|
+
WEEKDAY_SHORTNAME = {
|
|
33
|
+
'MO': WeekDay.MONDAY,
|
|
34
|
+
'TU': WeekDay.TUESDAY,
|
|
35
|
+
'WE': WeekDay.WEDNESDAY,
|
|
36
|
+
'TH': WeekDay.THURSDAY,
|
|
37
|
+
'FR': WeekDay.FRIDAY,
|
|
38
|
+
'SA': WeekDay.SATURDAY,
|
|
39
|
+
'SU': WeekDay.SUNDAY
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
MONTH_SHORTNAME = {
|
|
44
|
+
'jan': 1,
|
|
45
|
+
'feb': 2,
|
|
46
|
+
'mar': 3,
|
|
47
|
+
'apr': 4,
|
|
48
|
+
'may': 5,
|
|
49
|
+
'jun': 6,
|
|
50
|
+
'jul': 7,
|
|
51
|
+
'aug': 8,
|
|
52
|
+
'sep': 9,
|
|
53
|
+
'oct': 10,
|
|
54
|
+
'nov': 11,
|
|
55
|
+
'dec': 12,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
DATEMATCH = re.compile(r'^(?P<d>N|T|Y|P|M)(?P<n>[-+]?\d+)?(?P<b>b?)?$')
|