Skip to content

Commit 4829617

Browse files
committed
vibe coding
1 parent f7785c9 commit 4829617

9 files changed

Lines changed: 406 additions & 24 deletions

File tree

.github/copilot-instructions.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,4 @@ applyTo: '/**'
2121
* The documentation for ccy is available at `https://ccy.quantmid.com`
2222
* Documentation is built using [mkdocs](https://www.mkdocs.org/) and stored in the `docs/` directory. The documentation source files are written in markdown format.
2323
* Do not use em dashes (—) in documentation files or docstrings. Use colons, parentheses, or restructure the sentence instead.
24+
* To link to a class or function in documentation, use the mkdocstrings cross-reference notation: `[ClassName][module.path.ClassName]` (e.g. `[TradingCentre][ccy.tradingcentres.TradingCentre]`)

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ docs: ## Build mkdocs site
3838

3939
.PHONY: docs-serve
4040
docs-serve: ## Serve docs locally with live reload
41-
uv run mkdocs serve
41+
uv run mkdocs serve --livereload --watch ccy --watch docs
4242

4343
.PHONY: publish-docs
4444
publish-docs: ## Publish docs to github pages

ccy/core/daycounter.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,13 @@ class DayCounter(StrEnum):
1717
THIRTYE360 = "30E/360"
1818
ACTACT = "ACT/ACT"
1919

20-
def count(self, start: date, end: date) -> float:
21-
"""Count the number of days between 2 dates"""
22-
return date_diff(end, start).total_seconds() / 86400
23-
2420
def dcf(self, start: date, end: date) -> float:
2521
"""Day count fraction between 2 dates"""
2622
match self:
2723
case DayCounter.ACT360:
28-
return self.count(start, end) / 360.0
24+
return count_days(start, end) / 360.0
2925
case DayCounter.ACT365:
30-
return self.count(start, end) / 365.0
26+
return count_days(start, end) / 365.0
3127
case DayCounter.THIRTY360:
3228
return _thirty_360(start, end)
3329
case DayCounter.THIRTYE360:
@@ -38,6 +34,11 @@ def dcf(self, start: date, end: date) -> float:
3834
raise ValueError(f"Unknown day counter: {self}")
3935

4036

37+
def count_days(start: date, end: date) -> float:
38+
"""Count the number of days between 2 dates"""
39+
return date_diff(end, start).total_seconds() / 86400
40+
41+
4142
def _thirty_e360(start: date, end: date) -> float:
4243
d1 = min(start.day, 30)
4344
d2 = min(end.day, 30)

ccy/tradingcentres/__init__.py

Lines changed: 53 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,47 @@
11
from __future__ import annotations
22

3-
from dataclasses import dataclass, field
43
from datetime import date, timedelta
4+
from typing_extensions import Annotated, Doc
5+
56
import holidays
67
import holidays.countries
78
import holidays.financial
9+
from pydantic import BaseModel, Field
810

911
isoweekend = frozenset((6, 7))
1012
oneday = timedelta(days=1)
1113

1214
trading_centres: dict[str, TradingCentre] = {}
1315

1416

15-
def prevbizday(dte: date, nd: int = 1, tcs: str | None = None) -> date:
17+
def prevbizday(
18+
dte: Annotated[date, Doc("The reference date")],
19+
nd: Annotated[int, Doc("Number of business days to move back")] = 1,
20+
tcs: Annotated[str | None, Doc("Comma-separated trading centre codes")] = None,
21+
) -> date:
22+
"""Return the date nd business days before dte."""
1623
return centres(tcs).prevbizday(dte, nd)
1724

1825

19-
def nextbizday(dte: date, nd: int = 1, tcs: str | None = None) -> date:
26+
def nextbizday(
27+
dte: Annotated[date, Doc("The reference date")],
28+
nd: Annotated[
29+
int,
30+
Doc("Number of business days to move forward; 0 adjusts to next biz day"),
31+
] = 1,
32+
tcs: Annotated[str | None, Doc("Comma-separated trading centre codes")] = None,
33+
) -> date:
34+
"""Return the date nd business days after dte."""
2035
return centres(tcs).nextbizday(dte, nd)
2136

2237

23-
def centres(codes: str | None = None) -> TradingCentres:
38+
def centres(
39+
codes: Annotated[
40+
str | None, Doc("Comma-separated trading centre codes, e.g. 'LON,NY'")
41+
] = None,
42+
) -> TradingCentres:
43+
"""Return a [TradingCentres][ccy.tradingcentres.TradingCentres] instance
44+
for the given centre codes."""
2445
tcs = TradingCentres()
2546
if codes:
2647
lcs = codes.upper().replace(" ", "").split(",")
@@ -31,32 +52,44 @@ def centres(codes: str | None = None) -> TradingCentres:
3152
return tcs
3253

3354

34-
@dataclass
35-
class TradingCentre:
36-
code: str
37-
calendar: holidays.HolidayBase
55+
class TradingCentre(BaseModel, arbitrary_types_allowed=True):
56+
code: str = Field(description="The code of the trading centre")
57+
calendar: holidays.HolidayBase = Field(
58+
exclude=True,
59+
description="The holiday calendar of the trading centre",
60+
)
3861

39-
def isholiday(self, dte: date) -> bool:
62+
def isholiday(self, dte: Annotated[date, Doc("The date to check")]) -> bool:
63+
"""Return True if the date is a holiday."""
4064
return dte in self.calendar
4165

4266

43-
@dataclass
44-
class TradingCentres:
45-
centres: dict[str, TradingCentre] = field(default_factory=dict)
67+
class TradingCentres(BaseModel):
68+
centres: dict[str, TradingCentre] = Field(default_factory=dict)
4669

4770
@property
4871
def code(self) -> str:
72+
"""Comma-separated sorted codes of the trading centres."""
4973
return ",".join(sorted(self.centres))
5074

51-
def isbizday(self, dte: date) -> bool:
75+
def isbizday(self, dte: Annotated[date, Doc("The date to check")]) -> bool:
76+
"""Return True if the date is a business day across all centres."""
5277
if dte.isoweekday() in isoweekend:
5378
return False
5479
for c in self.centres.values():
5580
if c.isholiday(dte):
5681
return False
5782
return True
5883

59-
def nextbizday(self, dte: date, nd: int = 1) -> date:
84+
def nextbizday(
85+
self,
86+
dte: Annotated[date, Doc("The reference date")],
87+
nd: Annotated[
88+
int,
89+
Doc("Number of business days to move forward; 0 adjusts to next biz day"),
90+
] = 1,
91+
) -> date:
92+
"""Return the date nd business days after dte."""
6093
n = 0
6194
while not self.isbizday(dte):
6295
dte += oneday
@@ -66,7 +99,12 @@ def nextbizday(self, dte: date, nd: int = 1) -> date:
6699
n += 1
67100
return dte
68101

69-
def prevbizday(self, dte: date, nd: int = 1) -> date:
102+
def prevbizday(
103+
self,
104+
dte: Annotated[date, Doc("The reference date")],
105+
nd: Annotated[int, Doc("Number of business days to move back")] = 1,
106+
) -> date:
107+
"""Return the date nd business days before dte."""
70108
n = 0
71109
if nd < 0:
72110
return self.nextbizday(dte, -nd)

docs/trading-centres.md

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# Trading Centres
2+
3+
Trading centres provide business day calendars for financial date calculations.
4+
A [TradingCentre][ccy.tradingcentres.TradingCentre] wraps a single holiday calendar.
5+
[TradingCentres][ccy.tradingcentres.TradingCentres] combines multiple centres for joint business day calculations.
6+
7+
!!! note "Installation"
8+
This feature requires the `holidays` extra:
9+
```bash
10+
pip install ccy[holidays]
11+
```
12+
13+
## Available centres
14+
15+
| Code | Description |
16+
|------|-------------|
17+
| `TGT` | TARGET (European Central Bank) |
18+
| `LON` | London (United Kingdom) |
19+
| `NY` | New York (United States) |
20+
21+
## Functions
22+
23+
::: ccy.tradingcentres.centres
24+
25+
::: ccy.tradingcentres.nextbizday
26+
27+
::: ccy.tradingcentres.prevbizday
28+
29+
## Classes
30+
31+
::: ccy.tradingcentres.TradingCentre
32+
33+
::: ccy.tradingcentres.TradingCentres
34+
35+
## Usage
36+
37+
### Business day checks
38+
39+
```python
40+
from datetime import date
41+
from ccy.tradingcentres import centres
42+
43+
tcs = centres("TGT")
44+
tcs.isbizday(date(2024, 12, 25)) # False — Christmas
45+
tcs.isbizday(date(2024, 12, 24)) # True
46+
```
47+
48+
Combine multiple centres — a day is a business day only if it is in all of them:
49+
50+
```python
51+
tcs = centres("LON,NY")
52+
tcs.code # "LON,NY"
53+
tcs.isbizday(date(2024, 7, 4)) # False — US Independence Day
54+
```
55+
56+
### Next and previous business day
57+
58+
```python
59+
from ccy.tradingcentres import nextbizday, prevbizday
60+
from datetime import date
61+
62+
friday = date(2024, 12, 20)
63+
64+
nextbizday(friday) # date(2024, 12, 23) — skips weekend
65+
nextbizday(friday, nd=2) # date(2024, 12, 24)
66+
67+
prevbizday(friday) # date(2024, 12, 19)
68+
prevbizday(friday, nd=3) # date(2024, 12, 17)
69+
```
70+
71+
Pass a centre code to apply its holiday calendar:
72+
73+
```python
74+
nextbizday(date(2024, 12, 24), tcs="TGT") # date(2024, 12, 27) — skips Christmas
75+
```
76+
77+
### `nd=0` — adjust to business day
78+
79+
Passing `nd=0` to `nextbizday` adjusts the date forward to the next business day if it falls on a weekend or holiday, and leaves it unchanged otherwise:
80+
81+
```python
82+
saturday = date(2024, 12, 21)
83+
nextbizday(saturday, nd=0) # date(2024, 12, 23)
84+
nextbizday(friday, nd=0) # date(2024, 12, 20) — already a business day
85+
```

mkdocs.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,39 @@ theme:
2727
- navigation.path
2828
- content.code.copy
2929

30+
plugins:
31+
- search
32+
- mkdocstrings:
33+
handlers:
34+
python:
35+
options:
36+
show_root_heading: true
37+
show_if_no_docstring: true
38+
inherited_members: true
39+
members_order: source
40+
separate_signature: true
41+
unwrap_annotated: true
42+
filters:
43+
- "!^_[a-zA-Z0-9_]*"
44+
- "!^model_post_init$"
45+
merge_init_into_class: true
46+
docstring_section_style: spacy
47+
signature_crossrefs: true
48+
show_symbol_type_heading: true
49+
show_symbol_type_toc: true
50+
extensions:
51+
- griffe_typingdoc
52+
- griffe_pydantic:
53+
schema: false
54+
3055
markdown_extensions:
56+
- admonition
3157
- pymdownx.highlight:
3258
anchor_linenums: true
3359
- pymdownx.superfences
3460

3561
nav:
3662
- Home: index.md
3763
- Day Counters: daycounters.md
64+
- Trading Centres: trading-centres.md
3865
- Dates & Periods: dates.md

pyproject.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ requires-python = ">=3.11"
99
dependencies = [
1010
"python-dateutil>=2.9.0",
1111
"pycountry>=24.6.1",
12+
"pydantic>=2.0",
1213
"typing-extensions>=4.12",
1314
]
1415

@@ -35,8 +36,11 @@ dev = [
3536
"types-python-dateutil>=2.9.0.20241003",
3637
]
3738
book = [
39+
"griffe-pydantic>=1.0",
40+
"griffe-typingdoc>=0.2",
3841
"mkdocs>=1.6",
3942
"mkdocs-material>=9.0",
43+
"mkdocstrings[python]>=0.25",
4044
]
4145

4246
[build-system]

tests/test_dc.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,14 @@ def test_alldc():
1010
assert len(DayCounter) == 5
1111

1212

13-
def test_getdb():
13+
def test_dcf():
1414
for dc in DayCounter:
1515
assert dc.value == str(dc)
1616
start = date.today()
17-
assert dc.count(start, start + timedelta(days=1)) == 1
1817
assert dc.dcf(start, start + timedelta(days=1)) > 0
1918

19+
20+
def test_invalid_dc():
2021
with pytest.raises(ValueError):
2122
DayCounter("kaputt")
2223

0 commit comments

Comments
 (0)