finance-dates 0.2.0__tar.gz → 0.3.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 (22) hide show
  1. {finance_dates-0.2.0 → finance_dates-0.3.0}/Cargo.lock +2 -2
  2. {finance_dates-0.2.0 → finance_dates-0.3.0}/Cargo.toml +1 -1
  3. {finance_dates-0.2.0 → finance_dates-0.3.0}/PKG-INFO +3 -5
  4. {finance_dates-0.2.0 → finance_dates-0.3.0}/README.md +1 -4
  5. {finance_dates-0.2.0 → finance_dates-0.3.0}/finance_dates/__init__.py +6 -1
  6. finance_dates-0.3.0/finance_dates/_namespace.py +104 -0
  7. finance_dates-0.3.0/finance_dates/tests/test_polars_namespace.py +35 -0
  8. {finance_dates-0.2.0 → finance_dates-0.3.0}/pyproject.toml +4 -3
  9. {finance_dates-0.2.0 → finance_dates-0.3.0}/rust/Cargo.lock +1 -1
  10. {finance_dates-0.2.0 → finance_dates-0.3.0}/rust/Cargo.toml +1 -1
  11. {finance_dates-0.2.0 → finance_dates-0.3.0}/rust/coverage +486 -486
  12. {finance_dates-0.2.0 → finance_dates-0.3.0}/.gitignore +0 -0
  13. {finance_dates-0.2.0 → finance_dates-0.3.0}/LICENSE +0 -0
  14. {finance_dates-0.2.0 → finance_dates-0.3.0}/finance_dates/tests/test_all.py +0 -0
  15. {finance_dates-0.2.0 → finance_dates-0.3.0}/finance_dates/tests/test_docs.py +0 -0
  16. {finance_dates-0.2.0 → finance_dates-0.3.0}/rust/.config/nextest.toml +0 -0
  17. {finance_dates-0.2.0 → finance_dates-0.3.0}/rust/python/lib.rs +0 -0
  18. {finance_dates-0.2.0 → finance_dates-0.3.0}/rust/src/calendar.rs +0 -0
  19. {finance_dates-0.2.0 → finance_dates-0.3.0}/rust/src/holiday.rs +0 -0
  20. {finance_dates-0.2.0 → finance_dates-0.3.0}/rust/src/lib.rs +0 -0
  21. {finance_dates-0.2.0 → finance_dates-0.3.0}/rust/src/range.rs +0 -0
  22. {finance_dates-0.2.0 → finance_dates-0.3.0}/rust/src/trading_hours.rs +0 -0
@@ -68,7 +68,7 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
68
68
 
69
69
  [[package]]
70
70
  name = "finance-dates"
71
- version = "0.2.0"
71
+ version = "0.3.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.3.0"
82
82
  dependencies = [
83
83
  "chrono",
84
84
  "finance-dates",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "finance_dates_py"
3
- version = "0.2.0"
3
+ version = "0.3.0"
4
4
  edition = "2021"
5
5
  publish = false
6
6
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: finance-dates
3
- Version: 0.2.0
3
+ Version: 0.3.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
@@ -20,6 +20,7 @@ Classifier: Programming Language :: Python :: Implementation :: PyPy
20
20
  Classifier: Programming Language :: Rust
21
21
  Requires-Python: >=3.10
22
22
  Requires-Dist: finance-enums<0.6,>=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'
@@ -155,12 +156,9 @@ The Rust library crate is published as `finance-dates` and imported as
155
156
 
156
157
  ```toml
157
158
  [dependencies]
158
- finance-dates = "0.1.0"
159
+ finance-dates = "0.2.0"
159
160
  ```
160
161
 
161
162
  ```rust
162
163
  use finance_dates::{calendar_for_exchange, date_range};
163
164
  ```
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).
@@ -25,7 +25,12 @@ except ImportError: # pragma: no cover - sphinx fallback
25
25
  raise ImportError("finance_dates native extension is not available")
26
26
 
27
27
 
28
- __version__ = "0.2.0"
28
+ __version__ = "0.3.0"
29
+
30
+ try: # optional runtime dependency for expression namespace registration
31
+ from . import _namespace as _namespace # noqa: F401
32
+ except ImportError: # pragma: no cover - polars is optional outside expression use
33
+ pass
29
34
 
30
35
  __all__ = [
31
36
  "Calendar",
@@ -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,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)]
@@ -13,7 +13,7 @@ authors = [
13
13
  description = "Standard financial dates"
14
14
  readme = "README.md"
15
15
  license = { text = "Apache-2.0" }
16
- version = "0.2.0"
16
+ version = "0.3.0"
17
17
  requires-python = ">=3.10"
18
18
  keywords = []
19
19
 
@@ -33,6 +33,7 @@ classifiers = [
33
33
 
34
34
  dependencies = [
35
35
  "finance-enums>=0.5.1,<0.6",
36
+ "polars>=1.0",
36
37
  ]
37
38
 
38
39
  [project.optional-dependencies]
@@ -63,9 +64,9 @@ Repository = "https://github.com/prettygoodcapital/finance-dates"
63
64
  Homepage = "https://github.com/prettygoodcapital/finance-dates"
64
65
 
65
66
  [tool.bumpversion]
66
- current_version = "0.2.0"
67
+ current_version = "0.3.0"
67
68
  commit = true
68
- tag = true
69
+ tag = false
69
70
  commit_args = "-s"
70
71
 
71
72
  [[tool.bumpversion.files]]
@@ -68,7 +68,7 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
68
68
 
69
69
  [[package]]
70
70
  name = "finance-dates"
71
- version = "0.2.0"
71
+ version = "0.3.0"
72
72
  dependencies = [
73
73
  "chrono",
74
74
  "chrono-tz",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "finance-dates"
3
- version = "0.2.0"
3
+ version = "0.3.0"
4
4
  authors = ["PrettyGoodCapital <prettygoodcapital@gmail.com>"]
5
5
  edition = "2021"
6
6
  license = "Apache-2.0"