finance-dates 0.2.0__tar.gz → 0.4.0__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.
- {finance_dates-0.2.0 → finance_dates-0.4.0}/.gitignore +5 -2
- {finance_dates-0.2.0 → finance_dates-0.4.0}/Cargo.lock +32 -34
- {finance_dates-0.2.0 → finance_dates-0.4.0}/Cargo.toml +2 -2
- {finance_dates-0.2.0 → finance_dates-0.4.0}/PKG-INFO +9 -6
- {finance_dates-0.2.0 → finance_dates-0.4.0}/README.md +1 -4
- {finance_dates-0.2.0 → finance_dates-0.4.0}/finance_dates/__init__.py +9 -1
- finance_dates-0.4.0/finance_dates/_namespace.py +104 -0
- finance_dates-0.4.0/finance_dates/finance_dates.abi3.so +0 -0
- finance_dates-0.4.0/finance_dates/periods.py +33 -0
- {finance_dates-0.2.0 → finance_dates-0.4.0}/finance_dates/tests/test_all.py +12 -11
- finance_dates-0.4.0/finance_dates/tests/test_periods.py +33 -0
- finance_dates-0.4.0/finance_dates/tests/test_polars_namespace.py +35 -0
- finance_dates-0.4.0/finance_dates/tests/test_reference_calendars.py +182 -0
- {finance_dates-0.2.0 → finance_dates-0.4.0}/pyproject.toml +18 -7
- {finance_dates-0.2.0 → finance_dates-0.4.0}/rust/Cargo.lock +21 -22
- {finance_dates-0.2.0 → finance_dates-0.4.0}/rust/Cargo.toml +1 -1
- finance_dates-0.4.0/rust/coverage +4258 -0
- {finance_dates-0.2.0 → finance_dates-0.4.0}/rust/src/calendar.rs +2614 -423
- finance_dates-0.4.0/rust/src/holiday.rs +498 -0
- {finance_dates-0.2.0 → finance_dates-0.4.0}/rust/src/lib.rs +1 -0
- finance_dates-0.4.0/rust/src/lunar.rs +345 -0
- finance_dates-0.2.0/rust/coverage +0 -3461
- finance_dates-0.2.0/rust/src/holiday.rs +0 -239
- {finance_dates-0.2.0 → finance_dates-0.4.0}/LICENSE +0 -0
- {finance_dates-0.2.0 → finance_dates-0.4.0}/finance_dates/tests/test_docs.py +0 -0
- {finance_dates-0.2.0 → finance_dates-0.4.0}/rust/.config/nextest.toml +0 -0
- {finance_dates-0.2.0 → finance_dates-0.4.0}/rust/python/lib.rs +0 -0
- {finance_dates-0.2.0 → finance_dates-0.4.0}/rust/src/range.rs +0 -0
- {finance_dates-0.2.0 → finance_dates-0.4.0}/rust/src/trading_hours.rs +0 -0
|
@@ -147,8 +147,8 @@ emsdk
|
|
|
147
147
|
.DS_Store
|
|
148
148
|
|
|
149
149
|
# Hydra
|
|
150
|
-
outputs/
|
|
151
|
-
multirun/
|
|
150
|
+
/outputs/
|
|
151
|
+
/multirun/
|
|
152
152
|
|
|
153
153
|
# AI
|
|
154
154
|
ROADMAP.md
|
|
@@ -158,3 +158,6 @@ AGENTS.md
|
|
|
158
158
|
|
|
159
159
|
# Fact Sheets
|
|
160
160
|
data
|
|
161
|
+
|
|
162
|
+
# Calendar drift check artifact
|
|
163
|
+
drift_report.md
|
|
@@ -25,9 +25,9 @@ checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
|
|
25
25
|
|
|
26
26
|
[[package]]
|
|
27
27
|
name = "cc"
|
|
28
|
-
version = "1.2.
|
|
28
|
+
version = "1.2.65"
|
|
29
29
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
30
|
-
checksum = "
|
|
30
|
+
checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96"
|
|
31
31
|
dependencies = [
|
|
32
32
|
"find-msvc-tools",
|
|
33
33
|
"shlex",
|
|
@@ -41,9 +41,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
|
|
41
41
|
|
|
42
42
|
[[package]]
|
|
43
43
|
name = "chrono"
|
|
44
|
-
version = "0.4.
|
|
44
|
+
version = "0.4.45"
|
|
45
45
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
46
|
-
checksum = "
|
|
46
|
+
checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
|
|
47
47
|
dependencies = [
|
|
48
48
|
"iana-time-zone",
|
|
49
49
|
"num-traits",
|
|
@@ -68,7 +68,7 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
|
|
68
68
|
|
|
69
69
|
[[package]]
|
|
70
70
|
name = "finance-dates"
|
|
71
|
-
version = "0.
|
|
71
|
+
version = "0.4.0"
|
|
72
72
|
dependencies = [
|
|
73
73
|
"chrono",
|
|
74
74
|
"chrono-tz",
|
|
@@ -78,7 +78,7 @@ dependencies = [
|
|
|
78
78
|
|
|
79
79
|
[[package]]
|
|
80
80
|
name = "finance_dates_py"
|
|
81
|
-
version = "0.
|
|
81
|
+
version = "0.4.0"
|
|
82
82
|
dependencies = [
|
|
83
83
|
"chrono",
|
|
84
84
|
"finance-dates",
|
|
@@ -162,13 +162,12 @@ dependencies = [
|
|
|
162
162
|
|
|
163
163
|
[[package]]
|
|
164
164
|
name = "js-sys"
|
|
165
|
-
version = "0.3.
|
|
165
|
+
version = "0.3.102"
|
|
166
166
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
167
|
-
checksum = "
|
|
167
|
+
checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31"
|
|
168
168
|
dependencies = [
|
|
169
169
|
"cfg-if",
|
|
170
170
|
"futures-util",
|
|
171
|
-
"once_cell",
|
|
172
171
|
"wasm-bindgen",
|
|
173
172
|
]
|
|
174
173
|
|
|
@@ -180,9 +179,9 @@ checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
|
|
180
179
|
|
|
181
180
|
[[package]]
|
|
182
181
|
name = "log"
|
|
183
|
-
version = "0.4.
|
|
182
|
+
version = "0.4.33"
|
|
184
183
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
185
|
-
checksum = "
|
|
184
|
+
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
|
186
185
|
|
|
187
186
|
[[package]]
|
|
188
187
|
name = "num-traits"
|
|
@@ -240,9 +239,9 @@ dependencies = [
|
|
|
240
239
|
|
|
241
240
|
[[package]]
|
|
242
241
|
name = "pyo3"
|
|
243
|
-
version = "0.
|
|
242
|
+
version = "0.29.0"
|
|
244
243
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
245
|
-
checksum = "
|
|
244
|
+
checksum = "cd274650b21d4bfc26a0a47587962c1edb425f69287324355cd040c3ea66071c"
|
|
246
245
|
dependencies = [
|
|
247
246
|
"inventory",
|
|
248
247
|
"libc",
|
|
@@ -255,18 +254,18 @@ dependencies = [
|
|
|
255
254
|
|
|
256
255
|
[[package]]
|
|
257
256
|
name = "pyo3-build-config"
|
|
258
|
-
version = "0.
|
|
257
|
+
version = "0.29.0"
|
|
259
258
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
260
|
-
checksum = "
|
|
259
|
+
checksum = "c5e2a7d2f0d013342f295c048ad19237add5154a55b1c5a254c0ec93d4109078"
|
|
261
260
|
dependencies = [
|
|
262
261
|
"target-lexicon",
|
|
263
262
|
]
|
|
264
263
|
|
|
265
264
|
[[package]]
|
|
266
265
|
name = "pyo3-ffi"
|
|
267
|
-
version = "0.
|
|
266
|
+
version = "0.29.0"
|
|
268
267
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
269
|
-
checksum = "
|
|
268
|
+
checksum = "ca85c467da1bbc8d866eea5deff9cf29ea5f7785054a17da36e65bda9c05845b"
|
|
270
269
|
dependencies = [
|
|
271
270
|
"libc",
|
|
272
271
|
"pyo3-build-config",
|
|
@@ -274,9 +273,9 @@ dependencies = [
|
|
|
274
273
|
|
|
275
274
|
[[package]]
|
|
276
275
|
name = "pyo3-macros"
|
|
277
|
-
version = "0.
|
|
276
|
+
version = "0.29.0"
|
|
278
277
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
279
|
-
checksum = "
|
|
278
|
+
checksum = "9ac53762fd065daa3194dd09337a38bd793a188100fd1a9304c4ab312d901771"
|
|
280
279
|
dependencies = [
|
|
281
280
|
"proc-macro2",
|
|
282
281
|
"pyo3-macros-backend",
|
|
@@ -286,13 +285,12 @@ dependencies = [
|
|
|
286
285
|
|
|
287
286
|
[[package]]
|
|
288
287
|
name = "pyo3-macros-backend"
|
|
289
|
-
version = "0.
|
|
288
|
+
version = "0.29.0"
|
|
290
289
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
291
|
-
checksum = "
|
|
290
|
+
checksum = "4ca3a1557399783172dc5bf39cfca835157732532cba56b71d2292161e53b362"
|
|
292
291
|
dependencies = [
|
|
293
292
|
"heck",
|
|
294
293
|
"proc-macro2",
|
|
295
|
-
"pyo3-build-config",
|
|
296
294
|
"quote",
|
|
297
295
|
"syn",
|
|
298
296
|
]
|
|
@@ -314,9 +312,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
|
|
314
312
|
|
|
315
313
|
[[package]]
|
|
316
314
|
name = "shlex"
|
|
317
|
-
version = "
|
|
315
|
+
version = "2.0.1"
|
|
318
316
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
319
|
-
checksum = "
|
|
317
|
+
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
|
320
318
|
|
|
321
319
|
[[package]]
|
|
322
320
|
name = "siphasher"
|
|
@@ -332,9 +330,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
|
|
332
330
|
|
|
333
331
|
[[package]]
|
|
334
332
|
name = "syn"
|
|
335
|
-
version = "2.0.
|
|
333
|
+
version = "2.0.118"
|
|
336
334
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
337
|
-
checksum = "
|
|
335
|
+
checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
|
|
338
336
|
dependencies = [
|
|
339
337
|
"proc-macro2",
|
|
340
338
|
"quote",
|
|
@@ -355,9 +353,9 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
|
|
355
353
|
|
|
356
354
|
[[package]]
|
|
357
355
|
name = "wasm-bindgen"
|
|
358
|
-
version = "0.2.
|
|
356
|
+
version = "0.2.125"
|
|
359
357
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
360
|
-
checksum = "
|
|
358
|
+
checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a"
|
|
361
359
|
dependencies = [
|
|
362
360
|
"cfg-if",
|
|
363
361
|
"once_cell",
|
|
@@ -368,9 +366,9 @@ dependencies = [
|
|
|
368
366
|
|
|
369
367
|
[[package]]
|
|
370
368
|
name = "wasm-bindgen-macro"
|
|
371
|
-
version = "0.2.
|
|
369
|
+
version = "0.2.125"
|
|
372
370
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
373
|
-
checksum = "
|
|
371
|
+
checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d"
|
|
374
372
|
dependencies = [
|
|
375
373
|
"quote",
|
|
376
374
|
"wasm-bindgen-macro-support",
|
|
@@ -378,9 +376,9 @@ dependencies = [
|
|
|
378
376
|
|
|
379
377
|
[[package]]
|
|
380
378
|
name = "wasm-bindgen-macro-support"
|
|
381
|
-
version = "0.2.
|
|
379
|
+
version = "0.2.125"
|
|
382
380
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
383
|
-
checksum = "
|
|
381
|
+
checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd"
|
|
384
382
|
dependencies = [
|
|
385
383
|
"bumpalo",
|
|
386
384
|
"proc-macro2",
|
|
@@ -391,9 +389,9 @@ dependencies = [
|
|
|
391
389
|
|
|
392
390
|
[[package]]
|
|
393
391
|
name = "wasm-bindgen-shared"
|
|
394
|
-
version = "0.2.
|
|
392
|
+
version = "0.2.125"
|
|
395
393
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
396
|
-
checksum = "
|
|
394
|
+
checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f"
|
|
397
395
|
dependencies = [
|
|
398
396
|
"unicode-ident",
|
|
399
397
|
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "finance_dates_py"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.4.0"
|
|
4
4
|
edition = "2021"
|
|
5
5
|
publish = false
|
|
6
6
|
|
|
@@ -11,7 +11,7 @@ crate-type = ["cdylib"]
|
|
|
11
11
|
|
|
12
12
|
[dependencies]
|
|
13
13
|
finance-dates = { path = "./rust", version = "*" }
|
|
14
|
-
pyo3 = { version = "0.
|
|
14
|
+
pyo3 = { version = "0.29.0", features = ["abi3", "abi3-py310", "extension-module", "multiple-pymethods"] }
|
|
15
15
|
chrono = { version = "0.4", default-features = false, features = ["clock"] }
|
|
16
16
|
|
|
17
17
|
[profile.release]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: finance-dates
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Standard financial dates
|
|
5
5
|
Project-URL: Repository, https://github.com/prettygoodcapital/finance-dates
|
|
6
6
|
Project-URL: Homepage, https://github.com/prettygoodcapital/finance-dates
|
|
@@ -19,17 +19,20 @@ Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
|
19
19
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
20
20
|
Classifier: Programming Language :: Rust
|
|
21
21
|
Requires-Python: >=3.10
|
|
22
|
-
Requires-Dist: finance-enums<0.
|
|
22
|
+
Requires-Dist: finance-enums<0.7,>=0.5.1
|
|
23
|
+
Requires-Dist: polars>=1.0
|
|
23
24
|
Provides-Extra: develop
|
|
24
25
|
Requires-Dist: build; extra == 'develop'
|
|
25
26
|
Requires-Dist: bump-my-version; extra == 'develop'
|
|
26
27
|
Requires-Dist: check-dist; extra == 'develop'
|
|
27
28
|
Requires-Dist: cibuildwheel; extra == 'develop'
|
|
28
29
|
Requires-Dist: codespell; extra == 'develop'
|
|
30
|
+
Requires-Dist: exchange-calendars; extra == 'develop'
|
|
29
31
|
Requires-Dist: hatch-rs; extra == 'develop'
|
|
30
32
|
Requires-Dist: hatchling; extra == 'develop'
|
|
31
33
|
Requires-Dist: mdformat; extra == 'develop'
|
|
32
34
|
Requires-Dist: mdformat-tables>=1; extra == 'develop'
|
|
35
|
+
Requires-Dist: pandas-market-calendars; extra == 'develop'
|
|
33
36
|
Requires-Dist: pytest; extra == 'develop'
|
|
34
37
|
Requires-Dist: pytest-cov; extra == 'develop'
|
|
35
38
|
Requires-Dist: ruff; extra == 'develop'
|
|
@@ -38,6 +41,9 @@ Requires-Dist: ty; extra == 'develop'
|
|
|
38
41
|
Requires-Dist: uv; extra == 'develop'
|
|
39
42
|
Requires-Dist: wheel; extra == 'develop'
|
|
40
43
|
Requires-Dist: yardang; extra == 'develop'
|
|
44
|
+
Provides-Extra: reference
|
|
45
|
+
Requires-Dist: exchange-calendars; extra == 'reference'
|
|
46
|
+
Requires-Dist: pandas-market-calendars; extra == 'reference'
|
|
41
47
|
Description-Content-Type: text/markdown
|
|
42
48
|
|
|
43
49
|
# finance dates
|
|
@@ -155,12 +161,9 @@ The Rust library crate is published as `finance-dates` and imported as
|
|
|
155
161
|
|
|
156
162
|
```toml
|
|
157
163
|
[dependencies]
|
|
158
|
-
finance-dates = "0.
|
|
164
|
+
finance-dates = "0.2.0"
|
|
159
165
|
```
|
|
160
166
|
|
|
161
167
|
```rust
|
|
162
168
|
use finance_dates::{calendar_for_exchange, date_range};
|
|
163
169
|
```
|
|
164
|
-
|
|
165
|
-
> [!NOTE]
|
|
166
|
-
> This library was generated using [copier](https://copier.readthedocs.io/en/stable/) from the [Base Python Project Template repository](https://github.com/python-project-templates/base).
|
|
@@ -113,12 +113,9 @@ The Rust library crate is published as `finance-dates` and imported as
|
|
|
113
113
|
|
|
114
114
|
```toml
|
|
115
115
|
[dependencies]
|
|
116
|
-
finance-dates = "0.
|
|
116
|
+
finance-dates = "0.2.0"
|
|
117
117
|
```
|
|
118
118
|
|
|
119
119
|
```rust
|
|
120
120
|
use finance_dates::{calendar_for_exchange, date_range};
|
|
121
121
|
```
|
|
122
|
-
|
|
123
|
-
> [!NOTE]
|
|
124
|
-
> This library was generated using [copier](https://copier.readthedocs.io/en/stable/) from the [Base Python Project Template repository](https://github.com/python-project-templates/base).
|
|
@@ -7,6 +7,8 @@ Python module is a thin re-export layer.
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
+
from .periods import period_grid
|
|
11
|
+
|
|
10
12
|
try: # native extension is unavailable in some doc builds
|
|
11
13
|
from .finance_dates import (
|
|
12
14
|
COUNTRY_CODES,
|
|
@@ -25,7 +27,12 @@ except ImportError: # pragma: no cover - sphinx fallback
|
|
|
25
27
|
raise ImportError("finance_dates native extension is not available")
|
|
26
28
|
|
|
27
29
|
|
|
28
|
-
__version__ = "0.
|
|
30
|
+
__version__ = "0.4.0"
|
|
31
|
+
|
|
32
|
+
try: # optional runtime dependency for expression namespace registration
|
|
33
|
+
from . import _namespace as _namespace # noqa: F401
|
|
34
|
+
except ImportError: # pragma: no cover - polars is optional outside expression use
|
|
35
|
+
pass
|
|
29
36
|
|
|
30
37
|
__all__ = [
|
|
31
38
|
"Calendar",
|
|
@@ -33,4 +40,5 @@ __all__ = [
|
|
|
33
40
|
"COUNTRY_CODES3",
|
|
34
41
|
"EXCHANGE_CODES",
|
|
35
42
|
"date_range",
|
|
43
|
+
"period_grid",
|
|
36
44
|
]
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Polars ``.fdates`` namespace helpers for finance date operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import date
|
|
6
|
+
from functools import lru_cache
|
|
7
|
+
|
|
8
|
+
import polars as pl
|
|
9
|
+
|
|
10
|
+
from .finance_dates import Calendar
|
|
11
|
+
|
|
12
|
+
__all__ = []
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@lru_cache(maxsize=64)
|
|
16
|
+
def _calendar(exchange: str) -> Calendar:
|
|
17
|
+
return Calendar.from_exchange(exchange)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _shift_business_day_value(value: date | None, n: int, exchange: str) -> date | None:
|
|
21
|
+
if value is None:
|
|
22
|
+
return None
|
|
23
|
+
cal = _calendar(exchange)
|
|
24
|
+
out = value
|
|
25
|
+
step = cal.next_business_day if n >= 0 else cal.previous_business_day
|
|
26
|
+
for _ in range(abs(n)):
|
|
27
|
+
out = step(out)
|
|
28
|
+
return out
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _align_business_day_value(value: date | None, convention: str, exchange: str) -> date | None:
|
|
32
|
+
if value is None:
|
|
33
|
+
return None
|
|
34
|
+
cal = _calendar(exchange)
|
|
35
|
+
if cal.is_business_day(value):
|
|
36
|
+
return value
|
|
37
|
+
normalized = convention.strip().lower().replace("-", "_")
|
|
38
|
+
if normalized == "following":
|
|
39
|
+
return cal.next_business_day(value)
|
|
40
|
+
if normalized == "preceding":
|
|
41
|
+
return cal.previous_business_day(value)
|
|
42
|
+
if normalized == "modified_following":
|
|
43
|
+
following = cal.next_business_day(value)
|
|
44
|
+
return following if following.month == value.month else cal.previous_business_day(value)
|
|
45
|
+
if normalized == "modified_preceding":
|
|
46
|
+
preceding = cal.previous_business_day(value)
|
|
47
|
+
return preceding if preceding.month == value.month else cal.next_business_day(value)
|
|
48
|
+
raise ValueError(f"unsupported business-day convention: {convention!r}")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _day_count_fraction_value(start: date | None, end: date | None, convention: str) -> float | None:
|
|
52
|
+
if start is None or end is None:
|
|
53
|
+
return None
|
|
54
|
+
normalized = convention.strip().lower().replace("-", "_")
|
|
55
|
+
days = (end - start).days
|
|
56
|
+
if normalized in {"act_360", "actual_360"}:
|
|
57
|
+
return days / 360.0
|
|
58
|
+
if normalized in {"act_365", "act_365f", "actual_365", "actual_365f"}:
|
|
59
|
+
return days / 365.0
|
|
60
|
+
if normalized in {"act_252", "actual_252"}:
|
|
61
|
+
return days / 252.0
|
|
62
|
+
if normalized in {"30_360", "thirty_360", "bond_30_360"}:
|
|
63
|
+
d1 = min(start.day, 30)
|
|
64
|
+
d2 = min(end.day, 30) if d1 == 30 else end.day
|
|
65
|
+
return ((end.year - start.year) * 360 + (end.month - start.month) * 30 + (d2 - d1)) / 360.0
|
|
66
|
+
raise ValueError(f"unsupported day-count convention: {convention!r}")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@pl.api.register_expr_namespace("fdates")
|
|
70
|
+
class ExprFinanceDates:
|
|
71
|
+
def __init__(self, expr: pl.Expr) -> None:
|
|
72
|
+
self._expr = expr
|
|
73
|
+
|
|
74
|
+
def shift_business_days(self, n: int, *, exchange: str = "XNYS") -> pl.Expr:
|
|
75
|
+
return self._expr.map_elements(lambda value: _shift_business_day_value(value, n, exchange), return_dtype=pl.Date)
|
|
76
|
+
|
|
77
|
+
def align_to_business_day(self, convention: str = "following", *, exchange: str = "XNYS") -> pl.Expr:
|
|
78
|
+
return self._expr.map_elements(lambda value: _align_business_day_value(value, convention, exchange), return_dtype=pl.Date)
|
|
79
|
+
|
|
80
|
+
def day_count_fraction(self, end: pl.Expr, *, convention: str = "act_360") -> pl.Expr:
|
|
81
|
+
return pl.struct([self._expr.alias("start"), end.alias("end")]).map_elements(
|
|
82
|
+
lambda row: _day_count_fraction_value(row["start"], row["end"], convention),
|
|
83
|
+
return_dtype=pl.Float64,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@pl.api.register_series_namespace("fdates")
|
|
88
|
+
class SeriesFinanceDates:
|
|
89
|
+
def __init__(self, series: pl.Series) -> None:
|
|
90
|
+
self._series = series
|
|
91
|
+
|
|
92
|
+
def shift_business_days(self, n: int, *, exchange: str = "XNYS") -> pl.Series:
|
|
93
|
+
return self._series.map_elements(lambda value: _shift_business_day_value(value, n, exchange), return_dtype=pl.Date)
|
|
94
|
+
|
|
95
|
+
def align_to_business_day(self, convention: str = "following", *, exchange: str = "XNYS") -> pl.Series:
|
|
96
|
+
return self._series.map_elements(lambda value: _align_business_day_value(value, convention, exchange), return_dtype=pl.Date)
|
|
97
|
+
|
|
98
|
+
def day_count_fraction(self, end: pl.Series, *, convention: str = "act_360") -> pl.Series:
|
|
99
|
+
if len(self._series) != len(end):
|
|
100
|
+
raise ValueError("start and end series must have the same length")
|
|
101
|
+
values: list[float | None] = []
|
|
102
|
+
for start_value, end_value in zip(self._series, end):
|
|
103
|
+
values.append(_day_count_fraction_value(start_value, end_value, convention))
|
|
104
|
+
return pl.Series(self._series.name, values, dtype=pl.Float64)
|
|
Binary file
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Period-grid helpers shared across finance packages."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import polars as pl
|
|
6
|
+
from finance_enums import Frequency, to_frequency
|
|
7
|
+
|
|
8
|
+
__all__ = ["period_grid"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _period_rule(period: Frequency | str) -> str:
|
|
12
|
+
if isinstance(period, Frequency):
|
|
13
|
+
return period.polars_truncate
|
|
14
|
+
value = period.strip()
|
|
15
|
+
if not value:
|
|
16
|
+
raise ValueError("period must not be empty")
|
|
17
|
+
try:
|
|
18
|
+
return to_frequency(value).polars_truncate
|
|
19
|
+
except ValueError:
|
|
20
|
+
return value
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def period_grid(date: pl.Expr, period: Frequency | str | pl.Expr) -> pl.Expr:
|
|
24
|
+
"""Return a date-bucket expression for period-aware calculations.
|
|
25
|
+
|
|
26
|
+
``period`` accepts a ``finance_enums.Frequency`` value, any alias
|
|
27
|
+
accepted by ``finance_enums.to_frequency()``, a Polars duration
|
|
28
|
+
string accepted by ``dt.truncate()``, or a precomputed bucket
|
|
29
|
+
expression.
|
|
30
|
+
"""
|
|
31
|
+
if isinstance(period, pl.Expr):
|
|
32
|
+
return period
|
|
33
|
+
return date.dt.truncate(_period_rule(period))
|
|
@@ -155,12 +155,13 @@ def test_nyse_christmas_2022_observed_monday() -> None:
|
|
|
155
155
|
assert not cal.is_business_day(date(2022, 12, 26))
|
|
156
156
|
|
|
157
157
|
|
|
158
|
-
def
|
|
158
|
+
def test_nyse_juneteenth_first_year_2022() -> None:
|
|
159
159
|
cal = Calendar.from_exchange("XNYS")
|
|
160
|
-
#
|
|
160
|
+
# NYSE first observed Juneteenth in 2022; the market was open on 2021-06-18.
|
|
161
161
|
assert not cal.is_holiday(date(2020, 6, 19))
|
|
162
|
-
|
|
163
|
-
|
|
162
|
+
assert not cal.is_holiday(date(2021, 6, 18))
|
|
163
|
+
# 2022-06-19 was a Sunday, observed Monday 2022-06-20.
|
|
164
|
+
assert cal.is_holiday(date(2022, 6, 20))
|
|
164
165
|
|
|
165
166
|
|
|
166
167
|
def test_lse_easter_monday_2024() -> None:
|
|
@@ -438,22 +439,22 @@ def test_apac_lunch_break_sessions_are_exposed() -> None:
|
|
|
438
439
|
"XTKS": (
|
|
439
440
|
[(9, 0, 0, 11, 30, 0), (12, 30, 0, 15, 30, 0)],
|
|
440
441
|
[
|
|
441
|
-
(datetime(2026, 5,
|
|
442
|
-
(datetime(2026, 5,
|
|
442
|
+
(datetime(2026, 5, 26, 0, 0, tzinfo=timezone.utc), datetime(2026, 5, 26, 2, 30, tzinfo=timezone.utc)),
|
|
443
|
+
(datetime(2026, 5, 26, 3, 30, tzinfo=timezone.utc), datetime(2026, 5, 26, 6, 30, tzinfo=timezone.utc)),
|
|
443
444
|
],
|
|
444
445
|
),
|
|
445
446
|
"XHKG": (
|
|
446
447
|
[(9, 30, 0, 12, 0, 0), (13, 0, 0, 16, 0, 0)],
|
|
447
448
|
[
|
|
448
|
-
(datetime(2026, 5,
|
|
449
|
-
(datetime(2026, 5,
|
|
449
|
+
(datetime(2026, 5, 26, 1, 30, tzinfo=timezone.utc), datetime(2026, 5, 26, 4, 0, tzinfo=timezone.utc)),
|
|
450
|
+
(datetime(2026, 5, 26, 5, 0, tzinfo=timezone.utc), datetime(2026, 5, 26, 8, 0, tzinfo=timezone.utc)),
|
|
450
451
|
],
|
|
451
452
|
),
|
|
452
453
|
"XSHG": (
|
|
453
454
|
[(9, 30, 0, 11, 30, 0), (13, 0, 0, 15, 0, 0)],
|
|
454
455
|
[
|
|
455
|
-
(datetime(2026, 5,
|
|
456
|
-
(datetime(2026, 5,
|
|
456
|
+
(datetime(2026, 5, 26, 1, 30, tzinfo=timezone.utc), datetime(2026, 5, 26, 3, 30, tzinfo=timezone.utc)),
|
|
457
|
+
(datetime(2026, 5, 26, 5, 0, tzinfo=timezone.utc), datetime(2026, 5, 26, 7, 0, tzinfo=timezone.utc)),
|
|
457
458
|
],
|
|
458
459
|
),
|
|
459
460
|
}
|
|
@@ -461,7 +462,7 @@ def test_apac_lunch_break_sessions_are_exposed() -> None:
|
|
|
461
462
|
for code, (templates, windows) in cases.items():
|
|
462
463
|
cal = Calendar.from_exchange(code)
|
|
463
464
|
assert cal.regular_sessions == templates
|
|
464
|
-
assert cal.sessions(date(2026, 5,
|
|
465
|
+
assert cal.sessions(date(2026, 5, 26), date(2026, 5, 26)) == windows
|
|
465
466
|
|
|
466
467
|
|
|
467
468
|
def test_tokyo_lunch_gap_is_closed_and_boundaries_advance() -> None:
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import date
|
|
4
|
+
|
|
5
|
+
import polars as pl
|
|
6
|
+
import pytest
|
|
7
|
+
from finance_enums import Frequency
|
|
8
|
+
|
|
9
|
+
import finance_dates as fd
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_period_grid_accepts_frequency_alias_and_expr() -> None:
|
|
13
|
+
df = pl.DataFrame(
|
|
14
|
+
{
|
|
15
|
+
"date": [date(2024, 1, 2), date(2024, 1, 31), date(2024, 2, 1)],
|
|
16
|
+
"bucket": ["a", "a", "b"],
|
|
17
|
+
}
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
out = df.select(
|
|
21
|
+
fd.period_grid(pl.col("date"), Frequency.Month).alias("enum_month"),
|
|
22
|
+
fd.period_grid(pl.col("date"), "monthly").alias("alias_month"),
|
|
23
|
+
fd.period_grid(pl.col("date"), pl.col("bucket")).alias("expr_bucket"),
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
assert out["enum_month"].to_list() == [date(2024, 1, 1), date(2024, 1, 1), date(2024, 2, 1)]
|
|
27
|
+
assert out["alias_month"].to_list() == out["enum_month"].to_list()
|
|
28
|
+
assert out["expr_bucket"].to_list() == ["a", "a", "b"]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_period_grid_rejects_empty_period_string() -> None:
|
|
32
|
+
with pytest.raises(ValueError, match="must not be empty"):
|
|
33
|
+
pl.select(fd.period_grid(pl.lit(date(2024, 1, 1)), " "))
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import date
|
|
4
|
+
|
|
5
|
+
import polars as pl
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
import finance_dates # noqa: F401
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_polars_fdates_namespace_business_day_helpers() -> None:
|
|
12
|
+
df = pl.DataFrame(
|
|
13
|
+
{
|
|
14
|
+
"trade_date": [date(2024, 7, 3), date(2024, 7, 5)],
|
|
15
|
+
"settle_date": [date(2024, 7, 8), date(2024, 7, 10)],
|
|
16
|
+
}
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
out = df.with_columns(
|
|
20
|
+
pl.col("trade_date").fdates.shift_business_days(1, exchange="XNYS").alias("next_trade"),
|
|
21
|
+
pl.col("trade_date").fdates.align_to_business_day("following", exchange="XNYS").alias("aligned"),
|
|
22
|
+
pl.col("trade_date").fdates.day_count_fraction(pl.col("settle_date"), convention="act_360").alias("dcf"),
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
assert out["next_trade"].to_list() == [date(2024, 7, 5), date(2024, 7, 8)]
|
|
26
|
+
assert out["aligned"].to_list() == [date(2024, 7, 3), date(2024, 7, 5)]
|
|
27
|
+
assert out["dcf"].to_list() == pytest.approx([5 / 360, 5 / 360])
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_polars_fdates_namespace_modified_following() -> None:
|
|
31
|
+
df = pl.DataFrame({"d": [date(2024, 8, 31)]})
|
|
32
|
+
|
|
33
|
+
out = df.with_columns(pl.col("d").fdates.align_to_business_day("modified_following", exchange="XNYS").alias("rolled"))
|
|
34
|
+
|
|
35
|
+
assert out["rolled"].to_list() == [date(2024, 8, 30)]
|