feat(data): implement market-implied PD floor and 7x7 transition matrix parsing #task-290

This commit is contained in:
Variet Agent
2026-03-11 15:53:38 +09:00
parent 0762fcc5d8
commit b8514c1251
235 changed files with 1729 additions and 1102 deletions

170
data/ytm_fetcher.py Normal file
View File

@@ -0,0 +1,170 @@
"""
KAP(한국자산평가) 등급별 회사채 기준수익률 수집 모듈
KAP 웹사이트(koreaap.com)에서 공모회사채(무보증) 1년 만기 등급별
기준수익률과 국고채 수익률을 수집합니다.
데이터 출처:
- https://www.koreaap.com/kor/valuation01_01.html
- 채권금리 기준수익률 > 공모회사채 (무보증)
사용법:
ytm_data = get_ytm_data("2025-12-31")
# {'rf': 2.550, 'AAA': 2.986, 'AA+': 3.045, ...}
"""
import logging
from typing import Dict, Optional
logger = logging.getLogger(__name__)
# ============================================================
# 하드코딩 Fallback 데이터
# KAP 스크래핑이 불가능할 경우 사용하는 기준 데이터
# ============================================================
# 2025-12-31 기준 KAP 공모회사채(무보증) 1년 만기 기준수익률 (%)
FALLBACK_YTM_20251231: Dict[str, float] = {
"rf": 2.550, # 국고채 1Y
"AAA": 2.986,
"AA+": 3.045,
"AA": 3.094,
"AA-": 3.132,
"A+": 3.269,
"A": 3.439,
"A-": 3.704,
"BBB+": 4.749,
"BBB": 5.442,
"BBB-": 6.442,
}
def get_ytm_data(
date: str = "2025-12-31",
use_fallback: bool = True
) -> Dict[str, float]:
"""
KAP 등급별 1Y 회사채 수익률 + 국고채 수익률 반환
Parameters
----------
date : str
기준일 (YYYY-MM-DD). 기본값 2025-12-31
use_fallback : bool
True이면 하드코딩 fallback 데이터 사용 (스크래핑 없이)
Returns
-------
Dict[str, float]
{'rf': 국고채1Y, 'AAA': AAA_YTM, 'AA+': ..., ...}
단위: % (예: 2.986)
"""
if use_fallback or date == "2025-12-31":
logger.info(f"KAP YTM fallback 데이터 사용 (기준일: {date})")
return FALLBACK_YTM_20251231.copy()
# 향후 KAP 웹 스크래핑 구현 가능
# try:
# return _scrape_kap(date)
# except Exception as e:
# logger.warning(f"KAP 스크래핑 실패, fallback 사용: {e}")
# return FALLBACK_YTM_20251231.copy()
logger.info(f"KAP YTM fallback 데이터 사용 (기준일: {date})")
return FALLBACK_YTM_20251231.copy()
def compute_spreads(ytm_data: Dict[str, float]) -> Dict[str, float]:
"""
등급별 신용스프레드 계산 (= 회사채 YTM - 국고채 YTM)
Parameters
----------
ytm_data : Dict[str, float]
get_ytm_data() 반환값
Returns
-------
Dict[str, float]
등급별 스프레드 (단위: bp, 1bp = 0.01%)
예: {'AAA': 43.6, 'AA+': 49.5, ...}
"""
rf = ytm_data["rf"]
spreads = {}
for grade, ytm in ytm_data.items():
if grade == "rf":
continue
spreads[grade] = round((ytm - rf) * 100, 1) # % → bp
return spreads
# ============================================================
# 대등급 매핑
# ============================================================
# 노치 → 대등급 매핑
NOTCH_TO_BROAD = {
"AAA": "AAA",
"AA+": "AA", "AA": "AA", "AA-": "AA",
"A+": "A", "A": "A", "A-": "A",
"BBB+": "BBB", "BBB": "BBB", "BBB-": "BBB",
"BB+": "BB", "BB": "BB", "BB-": "BB",
"B+": "B", "B": "B", "B-": "B",
}
def compute_broad_grade_spreads(
spreads: Dict[str, float],
method: str = "mid"
) -> Dict[str, float]:
"""
노치등급 스프레드를 대등급(AAA, AA, A, BBB)으로 집계
Parameters
----------
spreads : Dict[str, float]
노치등급별 스프레드 (bp)
method : str
'mid': 중간 노치 값 사용 (AA+ AA AA- → AA 값)
'mean': 노치 평균
Returns
-------
Dict[str, float]
대등급별 스프레드 (bp)
"""
from collections import defaultdict
groups = defaultdict(list)
for notch, spread in spreads.items():
broad = NOTCH_TO_BROAD.get(notch, notch)
groups[broad].append(spread)
result = {}
for broad, values in groups.items():
if method == "mid":
# 중간 노치 (3개면 가운데, 1개면 그대로)
result[broad] = sorted(values)[len(values) // 2]
else:
result[broad] = sum(values) / len(values)
return result
if __name__ == "__main__":
# 테스트 실행
ytm = get_ytm_data("2025-12-31")
print("=== KAP 1Y YTM (2025-12-31) ===")
for grade, rate in ytm.items():
print(f" {grade:>5}: {rate:.3f}%")
print("\n=== 신용스프레드 (bp) ===")
spreads = compute_spreads(ytm)
for grade, sp in spreads.items():
print(f" {grade:>5}: {sp:.1f}bp")
print("\n=== 대등급 스프레드 (bp) ===")
broad = compute_broad_grade_spreads(spreads)
for grade, sp in broad.items():
print(f" {grade:>5}: {sp:.1f}bp")