Skip to content

Commit d18fe32

Browse files
authored
Merge pull request #112 from QuantStrategyLab/feat/cn-equity-calendar
Add CN equity trading calendar and domain constant
2 parents bc5351d + 823e382 commit d18fe32

6 files changed

Lines changed: 216 additions & 3 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "quant-platform-kit"
7-
version = "0.7.39"
7+
version = "0.7.40"
88
description = "Shared broker adapters, domain models, execution ports, and notification utilities for QuantStrategyLab strategies."
99
readme = "README.md"
1010
requires-python = ">=3.9"

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
setup(
55
name="quant-platform-kit",
6-
version="0.7.38",
6+
version="0.7.40",
77
description="Shared broker adapters, domain models, execution ports, and notification utilities for QuantStrategyLab strategies.",
88
package_dir={"": "src"},
99
packages=find_packages(where="src"),

src/quant_platform_kit/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
used by older strategy repositories.
55
"""
66

7-
__version__ = "0.7.38"
7+
__version__ = "0.7.40"
88

99
from .common.models import (
1010
ExecutionReport,
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
from __future__ import annotations
2+
3+
from datetime import date, datetime, timedelta
4+
from typing import Iterable, Iterator
5+
6+
CN_EQUITY_TIMEZONE = "Asia/Shanghai"
7+
8+
# Weekday holidays on SSE/SZSE calendars (2024-01-01 .. 2026-12-31), sourced from AkShare trade-date history.
9+
CN_EQUITY_HOLIDAYS: frozenset[str] = frozenset(
10+
{
11+
"2024-01-01",
12+
"2024-02-09",
13+
"2024-02-12",
14+
"2024-02-13",
15+
"2024-02-14",
16+
"2024-02-15",
17+
"2024-02-16",
18+
"2024-04-04",
19+
"2024-04-05",
20+
"2024-05-01",
21+
"2024-05-02",
22+
"2024-05-03",
23+
"2024-06-10",
24+
"2024-09-16",
25+
"2024-09-17",
26+
"2024-10-01",
27+
"2024-10-02",
28+
"2024-10-03",
29+
"2024-10-04",
30+
"2024-10-07",
31+
"2025-01-01",
32+
"2025-01-28",
33+
"2025-01-29",
34+
"2025-01-30",
35+
"2025-01-31",
36+
"2025-02-03",
37+
"2025-02-04",
38+
"2025-04-04",
39+
"2025-05-01",
40+
"2025-05-02",
41+
"2025-05-05",
42+
"2025-06-02",
43+
"2025-10-01",
44+
"2025-10-02",
45+
"2025-10-03",
46+
"2025-10-06",
47+
"2025-10-07",
48+
"2025-10-08",
49+
"2026-01-01",
50+
"2026-01-02",
51+
"2026-02-16",
52+
"2026-02-17",
53+
"2026-02-18",
54+
"2026-02-19",
55+
"2026-02-20",
56+
"2026-02-23",
57+
"2026-04-06",
58+
"2026-05-01",
59+
"2026-05-04",
60+
"2026-05-05",
61+
"2026-06-19",
62+
"2026-09-25",
63+
"2026-10-01",
64+
"2026-10-02",
65+
"2026-10-05",
66+
"2026-10-06",
67+
"2026-10-07",
68+
}
69+
)
70+
71+
72+
def normalize_cn_equity_date(value: date | datetime | str) -> date:
73+
if isinstance(value, datetime):
74+
return value.date()
75+
if isinstance(value, date):
76+
return value
77+
text = str(value).strip()
78+
if not text:
79+
raise ValueError("date value is required")
80+
return date.fromisoformat(text[:10])
81+
82+
83+
def is_cn_equity_weekday(value: date | datetime | str) -> bool:
84+
return normalize_cn_equity_date(value).weekday() < 5
85+
86+
87+
def is_cn_equity_holiday(value: date | datetime | str, *, holidays: Iterable[str] | None = None) -> bool:
88+
normalized = normalize_cn_equity_date(value).isoformat()
89+
holiday_set = frozenset(holidays) if holidays is not None else CN_EQUITY_HOLIDAYS
90+
return normalized in holiday_set
91+
92+
93+
def is_cn_equity_trading_day(value: date | datetime | str, *, holidays: Iterable[str] | None = None) -> bool:
94+
normalized = normalize_cn_equity_date(value)
95+
return is_cn_equity_weekday(normalized) and not is_cn_equity_holiday(normalized, holidays=holidays)
96+
97+
98+
def next_cn_equity_trading_day(
99+
value: date | datetime | str,
100+
*,
101+
holidays: Iterable[str] | None = None,
102+
) -> date:
103+
current = normalize_cn_equity_date(value)
104+
while True:
105+
current += timedelta(days=1)
106+
if is_cn_equity_trading_day(current, holidays=holidays):
107+
return current
108+
109+
110+
def add_cn_equity_trading_days(
111+
value: date | datetime | str,
112+
count: int,
113+
*,
114+
holidays: Iterable[str] | None = None,
115+
) -> date:
116+
if count < 0:
117+
raise ValueError("count must be non-negative")
118+
current = normalize_cn_equity_date(value)
119+
if count == 0:
120+
return current
121+
remaining = count
122+
while remaining > 0:
123+
current = next_cn_equity_trading_day(current, holidays=holidays)
124+
remaining -= 1
125+
return current
126+
127+
128+
def iter_cn_equity_trading_days(
129+
start: date | datetime | str,
130+
end: date | datetime | str,
131+
*,
132+
holidays: Iterable[str] | None = None,
133+
) -> Iterator[date]:
134+
current = normalize_cn_equity_date(start)
135+
last = normalize_cn_equity_date(end)
136+
if last < current:
137+
return
138+
while current <= last:
139+
if is_cn_equity_trading_day(current, holidays=holidays):
140+
yield current
141+
current += timedelta(days=1)
142+
143+
144+
def month_end_cn_equity_trading_day(
145+
value: date | datetime | str,
146+
*,
147+
holidays: Iterable[str] | None = None,
148+
) -> date:
149+
current = normalize_cn_equity_date(value)
150+
if current.month == 12:
151+
probe = date(current.year + 1, 1, 1) - timedelta(days=1)
152+
else:
153+
probe = date(current.year, current.month + 1, 1) - timedelta(days=1)
154+
while probe.month == current.month:
155+
if is_cn_equity_trading_day(probe, holidays=holidays):
156+
return probe
157+
probe -= timedelta(days=1)
158+
raise ValueError(f"no trading day found for month {current.year}-{current.month:02d}")
159+
160+
161+
__all__ = [
162+
"CN_EQUITY_HOLIDAYS",
163+
"CN_EQUITY_TIMEZONE",
164+
"add_cn_equity_trading_days",
165+
"is_cn_equity_holiday",
166+
"is_cn_equity_trading_day",
167+
"is_cn_equity_weekday",
168+
"iter_cn_equity_trading_days",
169+
"month_end_cn_equity_trading_day",
170+
"next_cn_equity_trading_day",
171+
"normalize_cn_equity_date",
172+
]

src/quant_platform_kit/common/strategies.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
)
1717

1818
US_EQUITY_DOMAIN = "us_equity"
19+
CN_EQUITY_DOMAIN = "cn_equity"
1920
CRYPTO_DOMAIN = "crypto"
2021

2122

tests/test_cn_equity_calendar.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from __future__ import annotations
2+
3+
import unittest
4+
from datetime import date
5+
6+
from quant_platform_kit.common.cn_equity_calendar import (
7+
add_cn_equity_trading_days,
8+
is_cn_equity_trading_day,
9+
month_end_cn_equity_trading_day,
10+
next_cn_equity_trading_day,
11+
)
12+
from quant_platform_kit.common.strategies import CN_EQUITY_DOMAIN
13+
14+
15+
class CnEquityCalendarTests(unittest.TestCase):
16+
def test_cn_equity_domain_constant(self) -> None:
17+
self.assertEqual(CN_EQUITY_DOMAIN, "cn_equity")
18+
19+
def test_weekend_is_not_trading_day(self) -> None:
20+
self.assertFalse(is_cn_equity_trading_day("2024-01-06"))
21+
22+
def test_new_year_holiday_is_not_trading_day(self) -> None:
23+
self.assertFalse(is_cn_equity_trading_day("2024-01-01"))
24+
25+
def test_regular_weekday_is_trading_day(self) -> None:
26+
self.assertTrue(is_cn_equity_trading_day("2024-01-02"))
27+
28+
def test_next_trading_day_skips_weekend_and_holiday(self) -> None:
29+
self.assertEqual(next_cn_equity_trading_day("2024-02-08"), date(2024, 2, 19))
30+
31+
def test_add_trading_days(self) -> None:
32+
self.assertEqual(add_cn_equity_trading_days("2024-01-02", 3), date(2024, 1, 5))
33+
34+
def test_month_end_trading_day(self) -> None:
35+
self.assertEqual(month_end_cn_equity_trading_day("2024-01-15"), date(2024, 1, 31))
36+
self.assertEqual(month_end_cn_equity_trading_day("2024-02-01"), date(2024, 2, 29))
37+
38+
39+
if __name__ == "__main__":
40+
unittest.main()

0 commit comments

Comments
 (0)