opendate 0.1.12__tar.gz → 0.1.19__tar.gz
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.
Potentially problematic release.
This version of opendate might be problematic. Click here for more details.
- opendate-0.1.19/PKG-INFO +762 -0
- opendate-0.1.19/README.md +731 -0
- {opendate-0.1.12 → opendate-0.1.19}/pyproject.toml +1 -1
- {opendate-0.1.12 → opendate-0.1.19}/src/date/__init__.py +54 -16
- {opendate-0.1.12 → opendate-0.1.19}/src/date/date.py +769 -714
- opendate-0.1.19/src/date/extras.py +58 -0
- opendate-0.1.12/PKG-INFO +0 -65
- opendate-0.1.12/README.md +0 -36
- opendate-0.1.12/src/date/extras.py +0 -90
- {opendate-0.1.12 → opendate-0.1.19}/LICENSE +0 -0
opendate-0.1.19/PKG-INFO
ADDED
|
@@ -0,0 +1,762 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: opendate
|
|
3
|
+
Version: 0.1.19
|
|
4
|
+
Summary: Python business datetimes
|
|
5
|
+
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Author: bissli
|
|
8
|
+
Author-email: bissli.xyz@protonmail.com
|
|
9
|
+
Requires-Python: >=3.9,<4.0
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Provides-Extra: test
|
|
19
|
+
Requires-Dist: asserts ; extra == "test"
|
|
20
|
+
Requires-Dist: bump2version ; extra == "test"
|
|
21
|
+
Requires-Dist: pandas-market-calendars
|
|
22
|
+
Requires-Dist: pdbpp ; extra == "test"
|
|
23
|
+
Requires-Dist: pendulum
|
|
24
|
+
Requires-Dist: pytest ; extra == "test"
|
|
25
|
+
Requires-Dist: regex
|
|
26
|
+
Requires-Dist: typing-extensions
|
|
27
|
+
Requires-Dist: wrapt
|
|
28
|
+
Project-URL: Repository, https://github.com/bissli/opendate
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# OpenDate
|
|
32
|
+
|
|
33
|
+
A powerful date and time library for Python, built on top of [Pendulum](https://github.com/sdispater/pendulum) with extensive business day support and financial date calculations.
|
|
34
|
+
|
|
35
|
+
## Overview
|
|
36
|
+
|
|
37
|
+
OpenDate extends Pendulum's excellent date/time handling with:
|
|
38
|
+
|
|
39
|
+
- **Business Day Calculations**: NYSE calendar by default (extensible to other calendars)
|
|
40
|
+
- **Enhanced Parsing**: Support for special codes, business day offsets, and multiple formats
|
|
41
|
+
- **Financial Functions**: Excel-compatible yearfrac, fractional months, and period calculations
|
|
42
|
+
- **Type Safety**: Comprehensive type annotations and conversion decorators
|
|
43
|
+
- **Timezone Handling**: Smart defaults and easy timezone conversions
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install opendate
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Quick Start
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from date import Date, DateTime, Time, Interval, EST
|
|
55
|
+
|
|
56
|
+
# Create dates
|
|
57
|
+
today = Date.today()
|
|
58
|
+
meeting = DateTime(2024, 1, 15, 14, 30, tzinfo=EST)
|
|
59
|
+
|
|
60
|
+
# Business day arithmetic
|
|
61
|
+
next_business_day = today.business().add(days=1) # or today.b.add(days=1)
|
|
62
|
+
five_business_days_ago = today.b.subtract(days=5)
|
|
63
|
+
|
|
64
|
+
# Parse various formats
|
|
65
|
+
date = Date.parse('2024-01-15')
|
|
66
|
+
date = Date.parse('01/15/2024')
|
|
67
|
+
date = Date.parse('T-3b') # 3 business days ago
|
|
68
|
+
|
|
69
|
+
# Intervals and ranges
|
|
70
|
+
interval = Interval(Date(2024, 1, 1), Date(2024, 12, 31))
|
|
71
|
+
business_days = interval.b.days # Count only business days
|
|
72
|
+
yearfrac = interval.yearfrac(0) # Financial year fraction
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Core Classes
|
|
76
|
+
|
|
77
|
+
### Date
|
|
78
|
+
|
|
79
|
+
Extended `pendulum.Date` with business day awareness:
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from date import Date, NYSE
|
|
83
|
+
|
|
84
|
+
# Create dates
|
|
85
|
+
today = Date.today()
|
|
86
|
+
date = Date(2024, 1, 15)
|
|
87
|
+
parsed = Date.parse('2024-01-15')
|
|
88
|
+
|
|
89
|
+
# Business day operations
|
|
90
|
+
date.business().add(days=5) # Add 5 business days
|
|
91
|
+
date.b.subtract(days=3) # Subtract 3 business days (shorthand)
|
|
92
|
+
date.b.start_of('month') # First business day of month
|
|
93
|
+
date.b.end_of('month') # Last business day of month
|
|
94
|
+
|
|
95
|
+
# Check business day status
|
|
96
|
+
date.is_business_day() # True if NYSE is open
|
|
97
|
+
date.business_hours() # (open_time, close_time) or (None, None)
|
|
98
|
+
|
|
99
|
+
# Period boundaries
|
|
100
|
+
date.start_of('week') # Monday (or next business day if .b)
|
|
101
|
+
date.end_of('month') # Last day of month
|
|
102
|
+
date.first_of('quarter') # First day of quarter
|
|
103
|
+
date.last_of('year') # December 31
|
|
104
|
+
|
|
105
|
+
# Navigation
|
|
106
|
+
date.next(WeekDay.FRIDAY) # Next Friday
|
|
107
|
+
date.previous(WeekDay.MONDAY) # Previous Monday
|
|
108
|
+
date.lookback('month') # One month ago
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### DateTime
|
|
112
|
+
|
|
113
|
+
Extended `pendulum.DateTime` with business day support:
|
|
114
|
+
|
|
115
|
+
**Important differences from Pendulum:**
|
|
116
|
+
- `DateTime.today()` returns start of day (00:00:00) instead of current time like `pendulum.today()`
|
|
117
|
+
- Methods preserve business status and entity when chaining
|
|
118
|
+
- `DateTime.instance()` adds UTC timezone by default if none specified
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
from date import DateTime, EST, UTC
|
|
122
|
+
|
|
123
|
+
# Create datetimes (timezone parameter available)
|
|
124
|
+
now = DateTime.now() # Current time in local timezone
|
|
125
|
+
now_utc = DateTime.now(tz=UTC) # Current time in UTC
|
|
126
|
+
dt = DateTime(2024, 1, 15, 9, 30, 0, tzinfo=EST)
|
|
127
|
+
parsed = DateTime.parse('2024-01-15T09:30:00') # Parsed with local timezone
|
|
128
|
+
|
|
129
|
+
# Business day operations (preserves time)
|
|
130
|
+
dt.b.add(days=1) # Next business day at 9:30 AM
|
|
131
|
+
dt.b.subtract(days=3) # Three business days ago at 9:30 AM
|
|
132
|
+
|
|
133
|
+
# Timezone conversions
|
|
134
|
+
dt.in_timezone(UTC) # Convert to UTC
|
|
135
|
+
dt.astimezone(EST) # Alternative syntax
|
|
136
|
+
|
|
137
|
+
# Combine date and time
|
|
138
|
+
from date import Date, Time
|
|
139
|
+
date = Date(2024, 1, 15)
|
|
140
|
+
time = Time(9, 30, tzinfo=EST)
|
|
141
|
+
dt = DateTime.combine(date, time, tzinfo=EST) # tzinfo parameter determines result timezone
|
|
142
|
+
|
|
143
|
+
# Extract components
|
|
144
|
+
dt.date() # Date(2024, 1, 15)
|
|
145
|
+
dt.time() # Time(9, 30, 0, tzinfo=EST)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Time
|
|
149
|
+
|
|
150
|
+
Extended `pendulum.Time` with enhanced parsing:
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
from date import Time, UTC
|
|
154
|
+
|
|
155
|
+
# Create times (timezone must be specified)
|
|
156
|
+
time = Time(9, 30, 0, tzinfo=UTC)
|
|
157
|
+
|
|
158
|
+
# Parsed times default to UTC timezone
|
|
159
|
+
parsed = Time.parse('9:30 AM') # Returns with tzinfo=UTC
|
|
160
|
+
parsed = Time.parse('14:30:45.123') # Returns with tzinfo=UTC
|
|
161
|
+
|
|
162
|
+
# Supported formats
|
|
163
|
+
Time.parse('9:30') # 09:30:00
|
|
164
|
+
Time.parse('9:30 PM') # 21:30:00
|
|
165
|
+
Time.parse('093015') # 09:30:15
|
|
166
|
+
Time.parse('14:30:45.123456') # With microseconds
|
|
167
|
+
|
|
168
|
+
# Timezone conversion
|
|
169
|
+
time.in_timezone(EST) # Convert to different timezone
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Interval
|
|
173
|
+
|
|
174
|
+
Extended `pendulum.Interval` with business day and financial calculations:
|
|
175
|
+
|
|
176
|
+
**Note:** Unlike Pendulum's `Interval.months` (which returns int), OpenDate's returns float with fractional months calculated from actual day counts.
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
from date import Date, Interval, NYSE
|
|
180
|
+
|
|
181
|
+
# Create intervals
|
|
182
|
+
start = Date(2024, 1, 1)
|
|
183
|
+
end = Date(2024, 12, 31)
|
|
184
|
+
interval = Interval(start, end)
|
|
185
|
+
|
|
186
|
+
# Basic properties
|
|
187
|
+
interval.days # 365 (calendar days)
|
|
188
|
+
interval.months # 12.0 (float with fractional months)
|
|
189
|
+
interval.years # 1 (always floors to int)
|
|
190
|
+
interval.quarters # 4.0 (approximate, based on days/365*4)
|
|
191
|
+
|
|
192
|
+
# Business day calculations
|
|
193
|
+
interval.b.days # ~252 (only business days)
|
|
194
|
+
interval.entity(NYSE).b.days # Explicitly set calendar entity
|
|
195
|
+
|
|
196
|
+
# Check which days are business days
|
|
197
|
+
business_flags = list(interval.is_business_day_range())
|
|
198
|
+
# [True, True, True, False, True, ...] # Mon-Fri are True, Sat-Sun are False
|
|
199
|
+
|
|
200
|
+
# Iterate over interval with different units
|
|
201
|
+
for date in interval.range('days'):
|
|
202
|
+
print(date) # Every day
|
|
203
|
+
|
|
204
|
+
for date in interval.range('weeks'):
|
|
205
|
+
print(date) # Every Monday
|
|
206
|
+
|
|
207
|
+
for date in interval.range('months'):
|
|
208
|
+
print(date) # First of each month
|
|
209
|
+
|
|
210
|
+
for date in interval.range('years'):
|
|
211
|
+
print(date) # January 1st of each year
|
|
212
|
+
|
|
213
|
+
# Business days only (works with 'days' unit)
|
|
214
|
+
for date in interval.b.range('days'):
|
|
215
|
+
print(date) # Only business days (skips weekends/holidays)
|
|
216
|
+
|
|
217
|
+
# Get period boundaries within interval
|
|
218
|
+
month_starts = interval.start_of('month')
|
|
219
|
+
# [2024-01-01, 2024-02-01, ..., 2024-12-01]
|
|
220
|
+
|
|
221
|
+
month_ends = interval.end_of('month')
|
|
222
|
+
# [2024-01-31, 2024-02-29, ..., 2024-12-31]
|
|
223
|
+
|
|
224
|
+
week_starts = interval.start_of('week')
|
|
225
|
+
# All Mondays in the interval
|
|
226
|
+
|
|
227
|
+
week_ends = interval.end_of('week')
|
|
228
|
+
# All Sundays in the interval
|
|
229
|
+
|
|
230
|
+
quarter_starts = interval.start_of('quarter')
|
|
231
|
+
# [2024-01-01, 2024-04-01, 2024-07-01, 2024-10-01]
|
|
232
|
+
|
|
233
|
+
year_starts = interval.start_of('year')
|
|
234
|
+
# [2024-01-01]
|
|
235
|
+
|
|
236
|
+
# Business day adjustments for period boundaries
|
|
237
|
+
# When a period start/end falls on a non-business day, it's automatically adjusted
|
|
238
|
+
interval_2018 = Interval(Date(2018, 1, 5), Date(2018, 4, 5))
|
|
239
|
+
|
|
240
|
+
# Start of month - shifts forward to next business day if needed
|
|
241
|
+
business_month_starts = interval_2018.b.start_of('month')
|
|
242
|
+
# [2018-01-02, 2018-02-01, 2018-03-01, 2018-04-02]
|
|
243
|
+
# Note: Jan 1 is holiday → Jan 2, Apr 1 is Sunday → Apr 2
|
|
244
|
+
|
|
245
|
+
# End of month - shifts backward to previous business day if needed
|
|
246
|
+
business_month_ends = interval_2018.b.end_of('month')
|
|
247
|
+
# [2018-01-31, 2018-02-28, 2018-03-29, 2018-04-30]
|
|
248
|
+
# Note: Mar 30 is Good Friday → Mar 29
|
|
249
|
+
|
|
250
|
+
# Works for all time units
|
|
251
|
+
interval_weeks = Interval(Date(2018, 1, 5), Date(2018, 1, 25))
|
|
252
|
+
business_week_starts = interval_weeks.b.start_of('week')
|
|
253
|
+
# [2018-01-02, 2018-01-08, 2018-01-16, 2018-01-22]
|
|
254
|
+
# Note: Jan 1 (Mon) is holiday → Jan 2, Jan 15 (Mon) is MLK Day → Jan 16
|
|
255
|
+
|
|
256
|
+
business_week_ends = interval_weeks.b.end_of('week')
|
|
257
|
+
# [2018-01-05, 2018-01-12, 2018-01-19, 2018-01-26]
|
|
258
|
+
# All Fridays (already business days)
|
|
259
|
+
|
|
260
|
+
interval_years = Interval(Date(2017, 6, 1), Date(2019, 6, 1))
|
|
261
|
+
business_year_starts = interval_years.b.start_of('year')
|
|
262
|
+
# [2017-01-03, 2018-01-02, 2019-01-02]
|
|
263
|
+
# Jan 1 falls on holidays/weekends → adjusted to first business day
|
|
264
|
+
|
|
265
|
+
business_year_ends = interval_years.b.end_of('year')
|
|
266
|
+
# [2017-12-29, 2018-12-31, 2019-12-31]
|
|
267
|
+
# Dec 31 on weekends → adjusted to last business day before
|
|
268
|
+
|
|
269
|
+
# Financial calculations (Excel-compatible)
|
|
270
|
+
interval.yearfrac(0) # US 30/360 basis (corporate bonds)
|
|
271
|
+
interval.yearfrac(1) # Actual/actual (Treasury bonds)
|
|
272
|
+
interval.yearfrac(2) # Actual/360 (money market)
|
|
273
|
+
interval.yearfrac(3) # Actual/365 (some bonds)
|
|
274
|
+
interval.yearfrac(4) # European 30/360 (Eurobonds)
|
|
275
|
+
|
|
276
|
+
# Negative intervals (when end < start)
|
|
277
|
+
backward = Interval(Date(2024, 12, 31), Date(2024, 1, 1))
|
|
278
|
+
backward.days # -365
|
|
279
|
+
backward.months # -12.0
|
|
280
|
+
backward.years # -1
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
## Enhanced Parsing
|
|
284
|
+
|
|
285
|
+
### Date Parsing
|
|
286
|
+
|
|
287
|
+
```python
|
|
288
|
+
from date import Date
|
|
289
|
+
|
|
290
|
+
# Standard formats
|
|
291
|
+
Date.parse('2024-01-15') # YYYY-MM-DD
|
|
292
|
+
Date.parse('01/15/2024') # MM/DD/YYYY
|
|
293
|
+
Date.parse('01/15/24') # MM/DD/YY
|
|
294
|
+
Date.parse('20240115') # YYYYMMDD
|
|
295
|
+
|
|
296
|
+
# Named months
|
|
297
|
+
Date.parse('15-Jan-2024') # DD-MON-YYYY
|
|
298
|
+
Date.parse('Jan 15, 2024') # MON DD, YYYY
|
|
299
|
+
Date.parse('January 15, 2024') # Full month name
|
|
300
|
+
Date.parse('15JAN2024') # Compact
|
|
301
|
+
|
|
302
|
+
# Special codes
|
|
303
|
+
Date.parse('T') # Today
|
|
304
|
+
Date.parse('Y') # Yesterday
|
|
305
|
+
Date.parse('P') # Previous business day
|
|
306
|
+
Date.parse('M') # Last day of previous month
|
|
307
|
+
|
|
308
|
+
# Date arithmetic with parsing
|
|
309
|
+
Date.parse('T-5') # 5 days ago
|
|
310
|
+
Date.parse('T+10') # 10 days from now
|
|
311
|
+
Date.parse('T-3b') # 3 business days ago
|
|
312
|
+
Date.parse('P+2b') # 2 business days after previous business day
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### DateTime Parsing
|
|
316
|
+
|
|
317
|
+
```python
|
|
318
|
+
from date import DateTime
|
|
319
|
+
|
|
320
|
+
# ISO 8601
|
|
321
|
+
DateTime.parse('2024-01-15T09:30:00')
|
|
322
|
+
DateTime.parse('2024-01-15T09:30:00Z')
|
|
323
|
+
|
|
324
|
+
# Date and time separated
|
|
325
|
+
DateTime.parse('2024-01-15 09:30:00')
|
|
326
|
+
DateTime.parse('01/15/2024 09:30:00')
|
|
327
|
+
|
|
328
|
+
# Unix timestamps
|
|
329
|
+
DateTime.parse(1640995200) # Seconds
|
|
330
|
+
DateTime.parse(1640995200000) # Milliseconds (auto-detected)
|
|
331
|
+
|
|
332
|
+
# Special codes (returns start of day)
|
|
333
|
+
DateTime.parse('T') # Today at 00:00:00
|
|
334
|
+
DateTime.parse('Y') # Yesterday at 00:00:00
|
|
335
|
+
DateTime.parse('P') # Previous business day at 00:00:00
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
## Business Day Operations
|
|
339
|
+
|
|
340
|
+
### Calendar Entities
|
|
341
|
+
|
|
342
|
+
```python
|
|
343
|
+
from date import Date, NYSE, Entity
|
|
344
|
+
|
|
345
|
+
# Use default NYSE calendar
|
|
346
|
+
date = Date.today().business().add(days=5)
|
|
347
|
+
|
|
348
|
+
# Set entity explicitly
|
|
349
|
+
date = Date.today().entity(NYSE).business().add(days=5)
|
|
350
|
+
|
|
351
|
+
# Check business day status
|
|
352
|
+
Date(2024, 1, 1).is_business_day() # False (New Year's Day)
|
|
353
|
+
Date(2024, 7, 4).is_business_day() # False (Independence Day)
|
|
354
|
+
Date(2024, 12, 25).is_business_day() # False (Christmas)
|
|
355
|
+
|
|
356
|
+
# Get market hours
|
|
357
|
+
Date(2024, 1, 15).business_hours() # (09:30 AM EST, 04:00 PM EST)
|
|
358
|
+
Date(2024, 1, 1).business_hours() # (None, None) - holiday
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
### Business Day Arithmetic
|
|
362
|
+
|
|
363
|
+
```python
|
|
364
|
+
from date import Date
|
|
365
|
+
|
|
366
|
+
date = Date(2024, 1, 15)
|
|
367
|
+
|
|
368
|
+
# Add/subtract business days
|
|
369
|
+
date.b.add(days=5) # 5 business days later
|
|
370
|
+
date.b.subtract(days=3) # 3 business days earlier
|
|
371
|
+
|
|
372
|
+
# Works across weekends and holidays
|
|
373
|
+
Date(2024, 3, 29).b.add(days=1) # 2024-04-01 (skips Good Friday + weekend)
|
|
374
|
+
|
|
375
|
+
# Period boundaries with business days
|
|
376
|
+
Date(2024, 7, 6).b.start_of('month') # 2024-07-05 (skips July 4th)
|
|
377
|
+
Date(2024, 4, 30).b.end_of('month') # 2024-04-30 (Tuesday, not Sunday)
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
## Financial Calculations
|
|
381
|
+
|
|
382
|
+
### Year Fractions
|
|
383
|
+
|
|
384
|
+
Excel-compatible year fraction calculations for financial formulas:
|
|
385
|
+
|
|
386
|
+
```python
|
|
387
|
+
from date import Date, Interval
|
|
388
|
+
|
|
389
|
+
start = Date(2024, 1, 1)
|
|
390
|
+
end = Date(2024, 12, 31)
|
|
391
|
+
interval = Interval(start, end)
|
|
392
|
+
|
|
393
|
+
# Different day count conventions
|
|
394
|
+
interval.yearfrac(0) # US (NASD) 30/360 - common for US corporate bonds
|
|
395
|
+
interval.yearfrac(1) # Actual/actual - US Treasury bonds
|
|
396
|
+
interval.yearfrac(2) # Actual/360 - money market instruments
|
|
397
|
+
interval.yearfrac(3) # Actual/365 - some corporate bonds
|
|
398
|
+
interval.yearfrac(4) # European 30/360 - Eurobonds
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
### Fractional Periods
|
|
402
|
+
|
|
403
|
+
**Note:** `Interval.months` returns float (unlike Pendulum which returns int).
|
|
404
|
+
Fractional months are calculated based on actual day counts within partial months.
|
|
405
|
+
|
|
406
|
+
```python
|
|
407
|
+
from date import Date, Interval
|
|
408
|
+
|
|
409
|
+
# Fractional months (not just integer count)
|
|
410
|
+
Interval(Date(2024, 1, 1), Date(2024, 2, 15)).months # ~1.5
|
|
411
|
+
Interval(Date(2024, 1, 15), Date(2024, 2, 14)).months # ~0.97
|
|
412
|
+
Interval(Date(2024, 1, 1), Date(2024, 2, 1)).months # 1.0 (exactly)
|
|
413
|
+
|
|
414
|
+
# Approximate quarters
|
|
415
|
+
Interval(Date(2024, 1, 1), Date(2024, 3, 31)).quarters # ~1.0
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
## Timezone Handling
|
|
419
|
+
|
|
420
|
+
```python
|
|
421
|
+
from date import DateTime, EST, UTC, GMT, LCL, Timezone
|
|
422
|
+
|
|
423
|
+
# Built-in timezones
|
|
424
|
+
dt_est = DateTime(2024, 1, 15, 9, 30, tzinfo=EST) # US/Eastern (same as America/New_York)
|
|
425
|
+
dt_utc = DateTime(2024, 1, 15, 14, 30, tzinfo=UTC) # UTC
|
|
426
|
+
dt_gmt = DateTime(2024, 1, 15, 14, 30, tzinfo=GMT) # GMT
|
|
427
|
+
dt_lcl = DateTime.now(tz=LCL) # Local timezone (from system)
|
|
428
|
+
|
|
429
|
+
# Custom timezones
|
|
430
|
+
tokyo = Timezone('Asia/Tokyo')
|
|
431
|
+
dt_tokyo = DateTime(2024, 1, 15, 23, 30, tzinfo=tokyo)
|
|
432
|
+
|
|
433
|
+
# Convert between timezones (preserves the instant in time, changes timezone)
|
|
434
|
+
dt_est.in_timezone(UTC) # 9:30 AM EST → 2:30 PM UTC
|
|
435
|
+
dt_utc.in_tz(EST) # 2:30 PM UTC → 9:30 AM EST (shorthand)
|
|
436
|
+
dt_est.astimezone(tokyo) # 9:30 AM EST → 11:30 PM JST
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
## Helper Functions and Decorators
|
|
440
|
+
|
|
441
|
+
### Type Conversion Decorators
|
|
442
|
+
|
|
443
|
+
```python
|
|
444
|
+
from date import expect_date, expect_datetime, expect_time
|
|
445
|
+
import datetime
|
|
446
|
+
|
|
447
|
+
@expect_date
|
|
448
|
+
def process_date(d):
|
|
449
|
+
return d.add(days=1)
|
|
450
|
+
|
|
451
|
+
# Automatically converts datetime.date to Date
|
|
452
|
+
result = process_date(datetime.date(2024, 1, 15)) # Returns Date object
|
|
453
|
+
|
|
454
|
+
@expect_datetime
|
|
455
|
+
def process_datetime(dt):
|
|
456
|
+
return dt.add(hours=1)
|
|
457
|
+
|
|
458
|
+
# Automatically converts to DateTime
|
|
459
|
+
result = process_datetime(datetime.datetime(2024, 1, 15, 9, 0))
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
### Timezone Decorators
|
|
463
|
+
|
|
464
|
+
```python
|
|
465
|
+
from date import prefer_utc_timezone, expect_utc_timezone
|
|
466
|
+
|
|
467
|
+
@prefer_utc_timezone
|
|
468
|
+
def get_timestamp():
|
|
469
|
+
return DateTime(2024, 1, 15, 9, 0) # Adds UTC if no timezone
|
|
470
|
+
|
|
471
|
+
@expect_utc_timezone
|
|
472
|
+
def get_utc_time():
|
|
473
|
+
return DateTime(2024, 1, 15, 9, 0, tzinfo=EST) # Forces to UTC
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
## Legacy Compatibility Functions
|
|
477
|
+
|
|
478
|
+
The `date.extras` module provides standalone functions for backward compatibility:
|
|
479
|
+
|
|
480
|
+
```python
|
|
481
|
+
from date import is_business_day, is_within_business_hours
|
|
482
|
+
from date import overlap_days
|
|
483
|
+
from date import Date, Interval
|
|
484
|
+
|
|
485
|
+
# Check current time against market hours
|
|
486
|
+
is_business_day() # Is today a business day?
|
|
487
|
+
is_within_business_hours() # Is it between market open/close?
|
|
488
|
+
|
|
489
|
+
# Calculate interval overlap
|
|
490
|
+
interval1 = (Date(2024, 1, 1), Date(2024, 1, 31))
|
|
491
|
+
interval2 = (Date(2024, 1, 15), Date(2024, 2, 15))
|
|
492
|
+
|
|
493
|
+
# Boolean check - do they overlap?
|
|
494
|
+
overlap_days(interval1, interval2) # True (they overlap)
|
|
495
|
+
|
|
496
|
+
# Get actual day count of overlap
|
|
497
|
+
overlap_days(interval1, interval2, days=True) # 17 (days of overlap)
|
|
498
|
+
|
|
499
|
+
# Works with Interval objects too
|
|
500
|
+
int1 = Interval(Date(2024, 1, 1), Date(2024, 1, 31))
|
|
501
|
+
int2 = Interval(Date(2024, 1, 15), Date(2024, 2, 15))
|
|
502
|
+
overlap_days(int1, int2, days=True) # 17
|
|
503
|
+
|
|
504
|
+
# Non-overlapping intervals return negative
|
|
505
|
+
int3 = Interval(Date(2024, 1, 1), Date(2024, 1, 10))
|
|
506
|
+
int4 = Interval(Date(2024, 1, 20), Date(2024, 1, 31))
|
|
507
|
+
overlap_days(int3, int4) # False
|
|
508
|
+
overlap_days(int3, int4, days=True) # -9 (negative = no overlap)
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
## Advanced Features
|
|
512
|
+
|
|
513
|
+
### Method Chaining
|
|
514
|
+
|
|
515
|
+
Date and DateTime operations preserve type and state (business mode, entity), allowing for clean method chaining:
|
|
516
|
+
|
|
517
|
+
```python
|
|
518
|
+
from date import Date, NYSE
|
|
519
|
+
|
|
520
|
+
result = Date(2024, 1, 15)\
|
|
521
|
+
.entity(NYSE)\
|
|
522
|
+
.business()\
|
|
523
|
+
.end_of('month')\
|
|
524
|
+
.subtract(days=5)\
|
|
525
|
+
.start_of('week')
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
### Custom Date Navigation
|
|
529
|
+
|
|
530
|
+
```python
|
|
531
|
+
from date import Date, WeekDay
|
|
532
|
+
|
|
533
|
+
date = Date(2024, 1, 15)
|
|
534
|
+
|
|
535
|
+
# Nth occurrence of weekday in period
|
|
536
|
+
date.nth_of('month', 3, WeekDay.WEDNESDAY) # 3rd Wednesday of month
|
|
537
|
+
|
|
538
|
+
# Named day navigation
|
|
539
|
+
date.next(WeekDay.FRIDAY) # Next Friday
|
|
540
|
+
date.previous(WeekDay.MONDAY) # Previous Monday
|
|
541
|
+
|
|
542
|
+
# Relative date finding
|
|
543
|
+
date.closest(date1, date2) # Closest of two dates
|
|
544
|
+
date.farthest(date1, date2) # Farthest of two dates
|
|
545
|
+
date.average(other_date) # Average of two dates
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
### Lookback Operations
|
|
549
|
+
|
|
550
|
+
```python
|
|
551
|
+
from date import Date
|
|
552
|
+
|
|
553
|
+
date = Date(2024, 1, 15)
|
|
554
|
+
|
|
555
|
+
date.lookback('day') # Yesterday
|
|
556
|
+
date.lookback('week') # One week ago
|
|
557
|
+
date.lookback('month') # One month ago
|
|
558
|
+
date.lookback('quarter') # One quarter ago
|
|
559
|
+
date.lookback('year') # One year ago
|
|
560
|
+
|
|
561
|
+
# With business mode
|
|
562
|
+
date.b.lookback('month') # One month ago, adjusted to business day
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
## Compatibility
|
|
566
|
+
|
|
567
|
+
OpenDate maintains compatibility with:
|
|
568
|
+
|
|
569
|
+
- **Pendulum**: Most Pendulum methods work as expected, with some notable differences:
|
|
570
|
+
- `Interval.months` returns float (with fractional months) instead of int
|
|
571
|
+
- `DateTime.today()` returns start of day (00:00:00) instead of current time
|
|
572
|
+
- Methods preserve business day status and entity when chaining
|
|
573
|
+
- **Python datetime**: Seamless conversion via `instance()` methods
|
|
574
|
+
- **Pandas**: Works with pandas Timestamp and datetime64
|
|
575
|
+
- **NumPy**: Supports numpy datetime64 conversion
|
|
576
|
+
|
|
577
|
+
```python
|
|
578
|
+
from date import Date, DateTime
|
|
579
|
+
import datetime
|
|
580
|
+
import pandas as pd
|
|
581
|
+
import numpy as np
|
|
582
|
+
|
|
583
|
+
# From Python datetime
|
|
584
|
+
Date.instance(datetime.date(2024, 1, 15))
|
|
585
|
+
DateTime.instance(datetime.datetime(2024, 1, 15, 9, 30))
|
|
586
|
+
|
|
587
|
+
# From Pandas
|
|
588
|
+
Date.instance(pd.Timestamp('2024-01-15'))
|
|
589
|
+
DateTime.instance(pd.Timestamp('2024-01-15 09:30:00'))
|
|
590
|
+
|
|
591
|
+
# From NumPy
|
|
592
|
+
Date.instance(np.datetime64('2024-01-15'))
|
|
593
|
+
DateTime.instance(np.datetime64('2024-01-15T09:30:00'))
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
## Why OpenDate?
|
|
597
|
+
|
|
598
|
+
### Over Pendulum
|
|
599
|
+
|
|
600
|
+
- Business day calculations with holiday awareness
|
|
601
|
+
- Financial functions (yearfrac, fractional periods)
|
|
602
|
+
- Enhanced parsing with special codes and business day offsets
|
|
603
|
+
- Built-in NYSE calendar (extensible to others)
|
|
604
|
+
- `Interval.months` returns float for precise financial calculations
|
|
605
|
+
- `DateTime.today()` returns start of day for consistent behavior
|
|
606
|
+
|
|
607
|
+
**Note:** Some Pendulum behavior is intentionally modified for financial use cases.
|
|
608
|
+
|
|
609
|
+
### Over datetime
|
|
610
|
+
|
|
611
|
+
- All benefits of Pendulum (better API, timezone handling, etc.)
|
|
612
|
+
- Plus all OpenDate business day features
|
|
613
|
+
- Cleaner syntax for common date operations
|
|
614
|
+
- Period-aware calculations
|
|
615
|
+
|
|
616
|
+
### Over pandas
|
|
617
|
+
|
|
618
|
+
- Lighter weight for non-DataFrame operations
|
|
619
|
+
- Better business day support
|
|
620
|
+
- Cleaner API for date arithmetic
|
|
621
|
+
- Financial functions built-in
|
|
622
|
+
|
|
623
|
+
## Examples
|
|
624
|
+
|
|
625
|
+
### Generate Month-End Dates
|
|
626
|
+
|
|
627
|
+
```python
|
|
628
|
+
from date import Date, Interval
|
|
629
|
+
|
|
630
|
+
interval = Interval(Date(2024, 1, 1), Date(2024, 12, 31))
|
|
631
|
+
|
|
632
|
+
# Get all month-end dates
|
|
633
|
+
month_ends = interval.end_of('month')
|
|
634
|
+
# [2024-01-31, 2024-02-29, ..., 2024-12-31]
|
|
635
|
+
|
|
636
|
+
# Get all month-start dates
|
|
637
|
+
month_starts = interval.start_of('month')
|
|
638
|
+
# [2024-01-01, 2024-02-01, ..., 2024-12-01]
|
|
639
|
+
|
|
640
|
+
# Get business day month-ends (adjusts for weekends/holidays)
|
|
641
|
+
business_month_ends = interval.b.end_of('month')
|
|
642
|
+
# Automatically adjusts any month-end that falls on non-business day
|
|
643
|
+
# to the previous business day
|
|
644
|
+
|
|
645
|
+
# Get business day month-starts (adjusts for weekends/holidays)
|
|
646
|
+
business_month_starts = interval.b.start_of('month')
|
|
647
|
+
# Automatically adjusts any month-start that falls on non-business day
|
|
648
|
+
# to the next business day
|
|
649
|
+
|
|
650
|
+
# Works with other periods too
|
|
651
|
+
quarter_ends = interval.end_of('quarter')
|
|
652
|
+
# [2024-03-31, 2024-06-30, 2024-09-30, 2024-12-31]
|
|
653
|
+
|
|
654
|
+
business_quarter_ends = interval.b.end_of('quarter')
|
|
655
|
+
# Quarter-ends adjusted to business days
|
|
656
|
+
|
|
657
|
+
week_starts = interval.start_of('week')
|
|
658
|
+
# All Mondays in 2024
|
|
659
|
+
|
|
660
|
+
business_week_starts = interval.b.start_of('week')
|
|
661
|
+
# All week starts adjusted to business days (skips holidays on Mondays)
|
|
662
|
+
|
|
663
|
+
# Partial year example
|
|
664
|
+
partial = Interval(Date(2024, 3, 15), Date(2024, 7, 20))
|
|
665
|
+
partial.end_of('month')
|
|
666
|
+
# [2024-03-31, 2024-04-30, 2024-05-31, 2024-06-30, 2024-07-31]
|
|
667
|
+
|
|
668
|
+
partial.b.end_of('month')
|
|
669
|
+
# Same as above but adjusted for any non-business days
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
### Calculate Business Days Between Dates
|
|
673
|
+
|
|
674
|
+
```python
|
|
675
|
+
from date import Date, Interval
|
|
676
|
+
|
|
677
|
+
start = Date(2024, 1, 1)
|
|
678
|
+
end = Date(2024, 12, 31)
|
|
679
|
+
interval = Interval(start, end)
|
|
680
|
+
|
|
681
|
+
# Count business days
|
|
682
|
+
business_days = interval.b.days # ~252
|
|
683
|
+
|
|
684
|
+
# Count calendar days
|
|
685
|
+
calendar_days = interval.days # 365
|
|
686
|
+
|
|
687
|
+
# Get list of which days are business days
|
|
688
|
+
is_bday = list(interval.is_business_day_range())
|
|
689
|
+
# [True, True, False, False, True, ...]
|
|
690
|
+
|
|
691
|
+
# Iterate only over business days
|
|
692
|
+
for bday in interval.b.range('days'):
|
|
693
|
+
print(f"{bday} is a business day")
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
### Find Next Options Expiration (3rd Friday)
|
|
697
|
+
|
|
698
|
+
```python
|
|
699
|
+
from date import Date, WeekDay
|
|
700
|
+
|
|
701
|
+
today = Date.today()
|
|
702
|
+
third_friday = today.add(months=1).start_of('month').nth_of('month', 3, WeekDay.FRIDAY)
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
### Working with Market Hours
|
|
706
|
+
|
|
707
|
+
```python
|
|
708
|
+
from date import DateTime, NYSE
|
|
709
|
+
|
|
710
|
+
now = DateTime.now(tz=NYSE.tz)
|
|
711
|
+
|
|
712
|
+
if now.is_business_day():
|
|
713
|
+
open_time, close_time = now.business_hours()
|
|
714
|
+
if open_time <= now <= close_time:
|
|
715
|
+
print("Market is open")
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
### Calculate Interest Accrual
|
|
719
|
+
|
|
720
|
+
```python
|
|
721
|
+
from date import Date, Interval
|
|
722
|
+
|
|
723
|
+
issue_date = Date(2024, 1, 15)
|
|
724
|
+
settlement_date = Date(2024, 6, 15)
|
|
725
|
+
coupon_rate = 0.05
|
|
726
|
+
|
|
727
|
+
# Using Actual/360 convention (basis 2)
|
|
728
|
+
days_fraction = Interval(issue_date, settlement_date).yearfrac(2)
|
|
729
|
+
accrued_interest = coupon_rate * days_fraction
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
## Testing
|
|
733
|
+
|
|
734
|
+
OpenDate includes comprehensive test coverage:
|
|
735
|
+
|
|
736
|
+
```bash
|
|
737
|
+
# Run all tests
|
|
738
|
+
pytest
|
|
739
|
+
|
|
740
|
+
# Run specific test files
|
|
741
|
+
pytest tests/test_date.py
|
|
742
|
+
pytest tests/test_business.py
|
|
743
|
+
pytest tests/test_interval.py
|
|
744
|
+
|
|
745
|
+
# Run with coverage
|
|
746
|
+
pytest --cov=date tests/
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
## Contributing
|
|
750
|
+
|
|
751
|
+
OpenDate is open source (MIT License). Contributions welcome!
|
|
752
|
+
|
|
753
|
+
## License
|
|
754
|
+
|
|
755
|
+
MIT License - see LICENSE file for details.
|
|
756
|
+
|
|
757
|
+
## Credits
|
|
758
|
+
|
|
759
|
+
Built on top of the excellent [Pendulum](https://github.com/sdispater/pendulum) library by Sébastien Eustace.
|
|
760
|
+
|
|
761
|
+
Business day calendars provided by [pandas-market-calendars](https://github.com/rsheftel/pandas_market_calendars).
|
|
762
|
+
|