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.
Files changed (29) hide show
  1. {finance_dates-0.2.0 → finance_dates-0.4.0}/.gitignore +5 -2
  2. {finance_dates-0.2.0 → finance_dates-0.4.0}/Cargo.lock +32 -34
  3. {finance_dates-0.2.0 → finance_dates-0.4.0}/Cargo.toml +2 -2
  4. {finance_dates-0.2.0 → finance_dates-0.4.0}/PKG-INFO +9 -6
  5. {finance_dates-0.2.0 → finance_dates-0.4.0}/README.md +1 -4
  6. {finance_dates-0.2.0 → finance_dates-0.4.0}/finance_dates/__init__.py +9 -1
  7. finance_dates-0.4.0/finance_dates/_namespace.py +104 -0
  8. finance_dates-0.4.0/finance_dates/finance_dates.abi3.so +0 -0
  9. finance_dates-0.4.0/finance_dates/periods.py +33 -0
  10. {finance_dates-0.2.0 → finance_dates-0.4.0}/finance_dates/tests/test_all.py +12 -11
  11. finance_dates-0.4.0/finance_dates/tests/test_periods.py +33 -0
  12. finance_dates-0.4.0/finance_dates/tests/test_polars_namespace.py +35 -0
  13. finance_dates-0.4.0/finance_dates/tests/test_reference_calendars.py +182 -0
  14. {finance_dates-0.2.0 → finance_dates-0.4.0}/pyproject.toml +18 -7
  15. {finance_dates-0.2.0 → finance_dates-0.4.0}/rust/Cargo.lock +21 -22
  16. {finance_dates-0.2.0 → finance_dates-0.4.0}/rust/Cargo.toml +1 -1
  17. finance_dates-0.4.0/rust/coverage +4258 -0
  18. {finance_dates-0.2.0 → finance_dates-0.4.0}/rust/src/calendar.rs +2614 -423
  19. finance_dates-0.4.0/rust/src/holiday.rs +498 -0
  20. {finance_dates-0.2.0 → finance_dates-0.4.0}/rust/src/lib.rs +1 -0
  21. finance_dates-0.4.0/rust/src/lunar.rs +345 -0
  22. finance_dates-0.2.0/rust/coverage +0 -3461
  23. finance_dates-0.2.0/rust/src/holiday.rs +0 -239
  24. {finance_dates-0.2.0 → finance_dates-0.4.0}/LICENSE +0 -0
  25. {finance_dates-0.2.0 → finance_dates-0.4.0}/finance_dates/tests/test_docs.py +0 -0
  26. {finance_dates-0.2.0 → finance_dates-0.4.0}/rust/.config/nextest.toml +0 -0
  27. {finance_dates-0.2.0 → finance_dates-0.4.0}/rust/python/lib.rs +0 -0
  28. {finance_dates-0.2.0 → finance_dates-0.4.0}/rust/src/range.rs +0 -0
  29. {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.62"
28
+ version = "1.2.65"
29
29
  source = "registry+https://github.com/rust-lang/crates.io-index"
30
- checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98"
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"
44
+ version = "0.4.45"
45
45
  source = "registry+https://github.com/rust-lang/crates.io-index"
46
- checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
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.2.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.2.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.99"
165
+ version = "0.3.102"
166
166
  source = "registry+https://github.com/rust-lang/crates.io-index"
167
- checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11"
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.30"
182
+ version = "0.4.33"
184
183
  source = "registry+https://github.com/rust-lang/crates.io-index"
185
- checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5"
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.28.3"
242
+ version = "0.29.0"
244
243
  source = "registry+https://github.com/rust-lang/crates.io-index"
245
- checksum = "91fd8e38a3b50ed1167fb981cd6fd60147e091784c427b8f7183a7ee32c31c12"
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.28.3"
257
+ version = "0.29.0"
259
258
  source = "registry+https://github.com/rust-lang/crates.io-index"
260
- checksum = "e368e7ddfdeb98c9bca7f8383be1648fd84ab466bf2bc015e94008db6d35611e"
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.28.3"
266
+ version = "0.29.0"
268
267
  source = "registry+https://github.com/rust-lang/crates.io-index"
269
- checksum = "7f29e10af80b1f7ccaf7f69eace800a03ecd13e883acfacc1e5d0988605f651e"
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.28.3"
276
+ version = "0.29.0"
278
277
  source = "registry+https://github.com/rust-lang/crates.io-index"
279
- checksum = "df6e520eff47c45997d2fc7dd8214b25dd1310918bbb2642156ef66a67f29813"
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.28.3"
288
+ version = "0.29.0"
290
289
  source = "registry+https://github.com/rust-lang/crates.io-index"
291
- checksum = "c4cdc218d835738f81c2338f822078af45b4afdf8b2e33cbb5916f108b813acb"
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 = "1.3.0"
315
+ version = "2.0.1"
318
316
  source = "registry+https://github.com/rust-lang/crates.io-index"
319
- checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
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.117"
333
+ version = "2.0.118"
336
334
  source = "registry+https://github.com/rust-lang/crates.io-index"
337
- checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
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.122"
356
+ version = "0.2.125"
359
357
  source = "registry+https://github.com/rust-lang/crates.io-index"
360
- checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409"
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.122"
369
+ version = "0.2.125"
372
370
  source = "registry+https://github.com/rust-lang/crates.io-index"
373
- checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6"
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.122"
379
+ version = "0.2.125"
382
380
  source = "registry+https://github.com/rust-lang/crates.io-index"
383
- checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e"
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.122"
392
+ version = "0.2.125"
395
393
  source = "registry+https://github.com/rust-lang/crates.io-index"
396
- checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437"
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.2.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.28.2", features = ["abi3", "abi3-py310", "extension-module", "multiple-pymethods"] }
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.2.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.6,>=0.5.1
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.1.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.1.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.2.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)
@@ -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 test_nyse_juneteenth_first_year_2021() -> None:
158
+ def test_nyse_juneteenth_first_year_2022() -> None:
159
159
  cal = Calendar.from_exchange("XNYS")
160
- # Not a holiday before 2021.
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
- # 2021 Jun 19 was Saturday → observed Friday Jun 18.
163
- assert cal.is_holiday(date(2021, 6, 18))
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, 25, 0, 0, tzinfo=timezone.utc), datetime(2026, 5, 25, 2, 30, tzinfo=timezone.utc)),
442
- (datetime(2026, 5, 25, 3, 30, tzinfo=timezone.utc), datetime(2026, 5, 25, 6, 30, tzinfo=timezone.utc)),
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, 25, 1, 30, tzinfo=timezone.utc), datetime(2026, 5, 25, 4, 0, tzinfo=timezone.utc)),
449
- (datetime(2026, 5, 25, 5, 0, tzinfo=timezone.utc), datetime(2026, 5, 25, 8, 0, tzinfo=timezone.utc)),
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, 25, 1, 30, tzinfo=timezone.utc), datetime(2026, 5, 25, 3, 30, tzinfo=timezone.utc)),
456
- (datetime(2026, 5, 25, 5, 0, tzinfo=timezone.utc), datetime(2026, 5, 25, 7, 0, tzinfo=timezone.utc)),
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, 25), date(2026, 5, 25)) == windows
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)]