Skip to content

Commit 88dc3be

Browse files
authored
Merge pull request #416 from easygap/fix/holidays-substitute-days
fix: 휴장일 fallback 대체공휴일·연말휴장 보강 — 60일 커버리지 분모 보호
2 parents 2e3f3ed + 76c38b4 commit 88dc3be

5 files changed

Lines changed: 80 additions & 19 deletions

File tree

‎config/holidays.yaml‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,27 +7,39 @@ holidays:
77
- '2026-01-28'
88
- '2026-01-29'
99
- '2026-03-01'
10+
- '2026-03-02'
1011
- '2026-05-05'
1112
- '2026-05-24'
13+
- '2026-05-25'
1214
- '2026-06-06'
1315
- '2026-08-15'
16+
- '2026-08-17'
1417
- '2026-09-24'
1518
- '2026-09-25'
1619
- '2026-09-26'
20+
- '2026-09-28'
1721
- '2026-10-03'
22+
- '2026-10-05'
1823
- '2026-10-09'
1924
- '2026-12-25'
25+
- '2026-12-31'
2026
- '2027-01-01'
2127
- '2027-02-10'
2228
- '2027-02-11'
2329
- '2027-02-12'
2430
- '2027-03-01'
2531
- '2027-05-05'
32+
- '2027-05-13'
2633
- '2027-06-06'
2734
- '2027-08-15'
35+
- '2027-08-16'
2836
- '2027-10-03'
2937
- '2027-10-04'
3038
- '2027-10-05'
3139
- '2027-10-06'
40+
- '2027-10-07'
3241
- '2027-10-09'
42+
- '2027-10-11'
3343
- '2027-12-25'
44+
- '2027-12-27'
45+
- '2027-12-31'

‎core/holidays_updater.py‎

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,24 +15,44 @@
1515
# 프로젝트 루트
1616
_PROJECT_ROOT = Path(__file__).resolve().parent.parent
1717

18-
# pykrx 미사용/실패 시 사용할 연도별 휴장일 (고정 공휴일 위주; 설·추석은 연도별 상이)
18+
# pykrx 미사용/실패 시 사용할 연도별 휴장일.
19+
# 대체공휴일 규정(설·추석 연휴, 3·1절, 광복절, 개천절, 한글날, 어린이날,
20+
# 부처님오신날, 성탄절은 주말 겹침 시 다음 평일 대체; 신정·현충일은 미적용)과
21+
# KRX 연말 휴장(마지막 거래일 12/31)을 포함한다 — 누락되면 휴장일이 거래일로
22+
# 계산돼 트랙레코드 커버리지 분모가 부풀고 스냅샷 귀속(가격 기준일)이 어긋난다.
1923
FALLBACK_BY_YEAR = {
2024
2025: {
21-
"2025-01-01", "2025-01-28", "2025-01-29", "2025-01-30",
22-
"2025-03-01", "2025-05-05", "2025-05-06", "2025-06-06",
23-
"2025-08-15", "2025-10-03", "2025-10-09", "2025-12-25",
25+
"2025-01-01", "2025-01-27", "2025-01-28", "2025-01-29", "2025-01-30", # 1/27 임시공휴일
26+
"2025-03-01", "2025-03-03", # 3·1절(토) 대체
27+
"2025-05-05", "2025-05-06", "2025-06-06",
28+
"2025-08-15", "2025-10-03",
29+
"2025-10-06", "2025-10-07", "2025-10-08", # 추석 연휴(일~화) + 대체
30+
"2025-10-09", "2025-12-25", "2025-12-31",
2431
},
2532
2026: {
2633
"2026-01-01", "2026-01-27", "2026-01-28", "2026-01-29",
27-
"2026-03-01", "2026-05-05", "2026-05-24", "2026-06-06",
28-
"2026-08-15", "2026-09-24", "2026-09-25", "2026-09-26",
29-
"2026-10-03", "2026-10-09", "2026-12-25",
34+
"2026-03-01", "2026-03-02", # 3·1절(일) 대체
35+
"2026-05-05",
36+
"2026-05-24", "2026-05-25", # 부처님오신날(일) 대체
37+
"2026-06-06", # 현충일(토) — 대체 미적용
38+
"2026-08-15", "2026-08-17", # 광복절(토) 대체
39+
"2026-09-24", "2026-09-25", "2026-09-26",
40+
"2026-09-28", # 추석 연휴 토요일 겹침 대체
41+
"2026-10-03", "2026-10-05", # 개천절(토) 대체
42+
"2026-10-09", "2026-12-25",
43+
"2026-12-31", # KRX 연말 휴장
3044
},
3145
2027: {
3246
"2027-01-01", "2027-02-10", "2027-02-11", "2027-02-12",
33-
"2027-03-01", "2027-05-05", "2027-06-06", "2027-08-15",
34-
"2027-10-03", "2027-10-04", "2027-10-05", "2027-10-06", "2027-10-09",
35-
"2027-12-25",
47+
"2027-03-01", "2027-05-05",
48+
"2027-05-13", # 부처님오신날(목)
49+
"2027-06-06", # 현충일(일) — 대체 미적용
50+
"2027-08-15", "2027-08-16", # 광복절(일) 대체
51+
"2027-10-03", "2027-10-04", "2027-10-05", "2027-10-06",
52+
"2027-10-07", # 개천절(일) 대체(추석 연휴 직후 평일)
53+
"2027-10-09", "2027-10-11", # 한글날(토) 대체
54+
"2027-12-25", "2027-12-27", # 성탄절(토) 대체
55+
"2027-12-31", # KRX 연말 휴장
3656
},
3757
}
3858

‎core/trading_hours.py‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@
1616
# 프로젝트 루트 (config 상위)
1717
_PROJECT_ROOT = Path(__file__).resolve().parent.parent
1818

19-
# 한국 증시 공휴일 — 최종 fallback (네트워크/파일 불가 시)
20-
KR_HOLIDAYS_FALLBACK = {
21-
"2026-01-01", "2026-01-27", "2026-01-28", "2026-01-29",
22-
"2026-03-01", "2026-05-05", "2026-05-24", "2026-06-06",
23-
"2026-08-15", "2026-09-24", "2026-09-25", "2026-09-26",
24-
"2026-10-03", "2026-10-09", "2026-12-25",
25-
}
19+
# 한국 증시 공휴일 — 최종 fallback (네트워크/파일 불가 시).
20+
# 단일 소스: core/holidays_updater.FALLBACK_BY_YEAR (대체공휴일·연말휴장 포함).
21+
# 별도 하드코딩을 두면 누락이 한쪽만 고쳐지는 드리프트가 생긴다 — 휴장일 누락은
22+
# 트랙레코드 커버리지 분모 부풀림·스냅샷 귀속 오차로 직결된다.
23+
from core.holidays_updater import FALLBACK_BY_YEAR as _FALLBACK_BY_YEAR
24+
25+
KR_HOLIDAYS_FALLBACK = set().union(*_FALLBACK_BY_YEAR.values())
2626

2727

2828
def _load_holidays() -> set:

‎tests/test_target_weight_paper_adapter.py‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8436,12 +8436,13 @@ def test_recommend_pilot_caps_matches_target_weight_plan(monkeypatch):
84368436
assert suggested["max_gross_exposure"] == 3_360_000
84378437
assert rec["suggested_preview"]["allowed"] is True
84388438
assert rec["valid_from"] == "2026-04-10"
8439-
assert rec["valid_to"] == "2026-07-03"
8439+
# 2026-05-25(부처님오신날 대체휴일) 반영으로 60영업일 만기가 1영업일 밀림
8440+
assert rec["valid_to"] == "2026-07-06"
84408441
assert rec["target_pilot_days"] == 60
84418442
assert "YYYY-MM-DD" not in rec["enable_command"]
84428443
assert "\n" not in rec["enable_command"]
84438444
assert "\\" not in rec["enable_command"]
8444-
assert "--from 2026-04-10 --to 2026-07-03" in rec["enable_command"]
8445+
assert "--from 2026-04-10 --to 2026-07-06" in rec["enable_command"]
84458446
assert "--max-orders 3 --max-positions 3" in rec["enable_command"]
84468447
assert "--max-notional 1260000 --max-exposure 3360000" in rec["enable_command"]
84478448

‎tests/test_trading_hours.py‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,31 @@ def test_market_open_time(th):
3333
dt = datetime(2026, 1, 5, 10, 0, 0) # 월 10:00
3434
if th.is_trading_day(dt):
3535
assert th.is_market_open(dt) is True
36+
37+
38+
def test_2026_substitute_holidays_are_non_trading_days():
39+
"""대체공휴일·연말휴장 누락 회귀 — 누락되면 휴장일이 거래일로 계산돼
40+
트랙레코드 커버리지 분모가 부풀고 스냅샷 귀속(가격 기준일)이 어긋난다.
41+
42+
2026: 3·1절(일)→3/2, 부처님오신날(일)→5/25, 광복절(토)→8/17,
43+
추석 토요일 겹침→9/28, 개천절(토)→10/5, KRX 연말 휴장 12/31.
44+
(현충일 6/6(토)은 대체 미적용 — 6/8은 거래일이어야 함)
45+
"""
46+
from datetime import datetime
47+
from core.trading_hours import TradingHours
48+
49+
th = TradingHours()
50+
for d in ("2026-03-02", "2026-05-25", "2026-08-17",
51+
"2026-09-28", "2026-10-05", "2026-12-31"):
52+
y, m, dd = map(int, d.split("-"))
53+
assert th.is_trading_day(datetime(y, m, dd)) is False, f"{d}는 휴장일이어야 함"
54+
# 현충일은 대체공휴일 미적용 — 다음 월요일은 정상 거래일
55+
assert th.is_trading_day(datetime(2026, 6, 8)) is True
56+
57+
58+
def test_fallback_sets_are_single_source():
59+
"""trading_hours의 최종 fallback이 holidays_updater와 단일 소스인지(드리프트 방지)."""
60+
from core.holidays_updater import FALLBACK_BY_YEAR
61+
from core.trading_hours import KR_HOLIDAYS_FALLBACK
62+
63+
assert KR_HOLIDAYS_FALLBACK == set().union(*FALLBACK_BY_YEAR.values())

0 commit comments

Comments
 (0)