- Belkin & Suchower (1998) credit cycle index (Zt) estimation via WLS - Vasicek single-factor conditional PD/TM model - Macro-Zt OLS regression with stepwise variable selection - 3-scenario (boom/neutral/recession) 50yr PD projection - Statistical validation suite (ADF, Ljung-Box, R2, ARCH) - BOK ECOS API integration with fallback data - Visualization module (7 chart types) - Detailed theoretical methodology docs/methodology.md
219 lines
6.1 KiB
Python
219 lines
6.1 KiB
Python
"""
|
||
Vasicek 단일팩터 모델 기반 조건부 PD 및 전이행렬 모듈
|
||
|
||
핵심 공식:
|
||
PD_PIT(Z) = Φ( (Φ⁻¹(PD_TTC) - √ρ · Z) / √(1-ρ) )
|
||
|
||
이 모듈은 Belkin & Suchower의 임계값 방식 대신,
|
||
Vasicek 공식을 직접 적용하는 간편 버전도 제공합니다.
|
||
|
||
참고문헌:
|
||
- Vasicek, O. (2002). "The Distribution of Loan Portfolio Value"
|
||
- Basel Committee (2005). "An Explanatory Note on the Basel II IRB Risk Weight Functions"
|
||
- Merton, R.C. (1974). "On the Pricing of Corporate Debt"
|
||
"""
|
||
|
||
import numpy as np
|
||
from scipy.stats import norm
|
||
from typing import Optional
|
||
import logging
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
def conditional_pd(pd_ttc: float, z: float, rho: float) -> float:
|
||
"""
|
||
Vasicek 공식으로 PIT PD 계산
|
||
|
||
PD_PIT(Z) = Φ( (Φ⁻¹(PD_TTC) - √ρ · Z) / √(1-ρ) )
|
||
|
||
Parameters
|
||
----------
|
||
pd_ttc : float - TTC (Through-the-Cycle) 부도확률
|
||
z : float - 체계적 요인 (Z > 0: 호황, Z < 0: 불황)
|
||
rho : float - 자산상관계수 (0 < ρ < 1)
|
||
|
||
Returns
|
||
-------
|
||
float : PIT (Point-in-Time) 부도확률
|
||
|
||
Examples
|
||
--------
|
||
>>> conditional_pd(0.02, 0, 0.20) # Z=0이면 PD_PIT = PD_TTC
|
||
0.02
|
||
>>> conditional_pd(0.02, -2, 0.20) # 불황시 PD 상승
|
||
0.1016...
|
||
>>> conditional_pd(0.02, 2, 0.20) # 호황시 PD 하락
|
||
0.0024...
|
||
"""
|
||
if pd_ttc <= 0:
|
||
return 0.0
|
||
if pd_ttc >= 1:
|
||
return 1.0
|
||
|
||
sqrt_rho = np.sqrt(rho)
|
||
sqrt_1_rho = np.sqrt(1.0 - rho)
|
||
|
||
numerator = norm.ppf(pd_ttc) - sqrt_rho * z
|
||
pd_pit = norm.cdf(numerator / sqrt_1_rho)
|
||
|
||
return float(np.clip(pd_pit, 0.0, 1.0))
|
||
|
||
|
||
def conditional_pd_array(pd_ttc_array: np.ndarray, z: float, rho: float) -> np.ndarray:
|
||
"""
|
||
벡터화된 Vasicek 공식 (등급별 TTC PD 배열 → PIT PD 배열)
|
||
"""
|
||
pd_ttc_clipped = np.clip(pd_ttc_array, 1e-10, 1.0 - 1e-10)
|
||
|
||
sqrt_rho = np.sqrt(rho)
|
||
sqrt_1_rho = np.sqrt(1.0 - rho)
|
||
|
||
numerator = norm.ppf(pd_ttc_clipped) - sqrt_rho * z
|
||
pd_pit = norm.cdf(numerator / sqrt_1_rho)
|
||
|
||
return np.clip(pd_pit, 0.0, 1.0)
|
||
|
||
|
||
def conditional_transition_matrix(
|
||
ttc_tm: np.ndarray,
|
||
z: float,
|
||
rho: float
|
||
) -> np.ndarray:
|
||
"""
|
||
임계값 기반 Z-조건부 전이행렬 산출
|
||
|
||
TTC 전이행렬로부터 누적확률 임계값을 산출하고,
|
||
Z 값에 따라 조건부 전이확률을 계산합니다.
|
||
|
||
Parameters
|
||
----------
|
||
ttc_tm : np.ndarray - N×N TTC 전이행렬
|
||
z : float - 체계적 요인
|
||
rho : float - 자산상관계수
|
||
|
||
Returns
|
||
-------
|
||
np.ndarray : N×N 조건부 전이행렬
|
||
"""
|
||
n = ttc_tm.shape[0]
|
||
sqrt_rho = np.sqrt(rho)
|
||
sqrt_1_rho = np.sqrt(1.0 - rho)
|
||
|
||
# 임계값 산출 (누적확률 → Φ⁻¹)
|
||
thresholds = np.full((n, n), np.inf)
|
||
for i in range(n):
|
||
cum_prob = 0.0
|
||
for j in range(n - 1):
|
||
cum_prob += ttc_tm[i, j]
|
||
cum_prob_clipped = np.clip(cum_prob, 1e-10, 1.0 - 1e-10)
|
||
thresholds[i, j] = norm.ppf(cum_prob_clipped)
|
||
|
||
# 조건부 전이행렬 계산
|
||
cond_tm = np.zeros((n, n))
|
||
|
||
for i in range(n - 1):
|
||
for j in range(n):
|
||
d_upper = thresholds[i, j]
|
||
upper = norm.cdf((d_upper - sqrt_rho * z) / sqrt_1_rho)
|
||
|
||
if j == 0:
|
||
lower = 0.0
|
||
else:
|
||
d_lower = thresholds[i, j - 1]
|
||
lower = norm.cdf((d_lower - sqrt_rho * z) / sqrt_1_rho)
|
||
|
||
cond_tm[i, j] = max(upper - lower, 0.0)
|
||
|
||
# 행 합 정규화
|
||
row_sum = cond_tm[i].sum()
|
||
if row_sum > 0:
|
||
cond_tm[i] /= row_sum
|
||
|
||
# D행: 흡수상태
|
||
cond_tm[-1, -1] = 1.0
|
||
|
||
return cond_tm
|
||
|
||
|
||
def multi_period_pd(
|
||
annual_tm: np.ndarray,
|
||
horizon: int,
|
||
initial_grade_idx: Optional[int] = None
|
||
) -> np.ndarray:
|
||
"""
|
||
전이행렬 거듭제곱으로 다기간 누적/한계 PD 계산
|
||
|
||
Parameters
|
||
----------
|
||
annual_tm : np.ndarray - 1년 전이행렬
|
||
horizon : int - 예측 기간 (년)
|
||
initial_grade_idx : int - 특정 등급만 계산 (None이면 전체)
|
||
|
||
Returns
|
||
-------
|
||
np.ndarray
|
||
shape (horizon, N-1): 연도별 각 등급의 누적 PD
|
||
또는 shape (horizon,): 특정 등급의 누적 PD
|
||
"""
|
||
n = annual_tm.shape[0]
|
||
cumulative_tm = np.eye(n)
|
||
|
||
cumulative_pds = []
|
||
for t in range(1, horizon + 1):
|
||
cumulative_tm = cumulative_tm @ annual_tm
|
||
# 부도열(마지막 열)이 누적 PD
|
||
if initial_grade_idx is not None:
|
||
cumulative_pds.append(cumulative_tm[initial_grade_idx, -1])
|
||
else:
|
||
cumulative_pds.append(cumulative_tm[:-1, -1].copy())
|
||
|
||
return np.array(cumulative_pds)
|
||
|
||
|
||
def marginal_pd_from_cumulative(cumulative_pds: np.ndarray) -> np.ndarray:
|
||
"""
|
||
누적 PD에서 한계 PD(Marginal PD) 계산
|
||
|
||
Marginal PD(t) = Cumulative PD(t) - Cumulative PD(t-1)
|
||
"""
|
||
if cumulative_pds.ndim == 1:
|
||
marginal = np.diff(cumulative_pds, prepend=0.0)
|
||
else:
|
||
first_row = np.zeros((1, cumulative_pds.shape[1]))
|
||
marginal = np.diff(cumulative_pds, axis=0, prepend=first_row)
|
||
|
||
return np.maximum(marginal, 0.0)
|
||
|
||
|
||
def survival_probability(cumulative_pds: np.ndarray) -> np.ndarray:
|
||
"""생존확률 = 1 - 누적 PD"""
|
||
return 1.0 - cumulative_pds
|
||
|
||
|
||
def annualized_pd(cumulative_pd: float, horizon: int) -> float:
|
||
"""
|
||
누적 PD를 연환산 PD로 변환
|
||
|
||
AnnualizedPD = 1 - (1 - CumulativePD)^(1/horizon)
|
||
"""
|
||
if cumulative_pd >= 1.0:
|
||
return 1.0
|
||
return 1.0 - (1.0 - cumulative_pd) ** (1.0 / horizon)
|
||
|
||
|
||
def worst_case_pd(pd_ttc: float, rho: float, confidence: float = 0.999) -> float:
|
||
"""
|
||
Basel II IRB 방식 Worst-Case PD (99.9% 신뢰수준)
|
||
|
||
WCPD = Φ( (Φ⁻¹(PD) + √ρ · Φ⁻¹(confidence)) / √(1-ρ) )
|
||
"""
|
||
if pd_ttc <= 0:
|
||
return 0.0
|
||
|
||
sqrt_rho = np.sqrt(rho)
|
||
sqrt_1_rho = np.sqrt(1.0 - rho)
|
||
|
||
numerator = norm.ppf(pd_ttc) + sqrt_rho * norm.ppf(confidence)
|
||
return float(norm.cdf(numerator / sqrt_1_rho))
|