""" Lifetime PD (50년) 메인 실행 파일 전체 파이프라인: 1. 데이터 로딩 (전이행렬 + 거시경제변수) 2. Belkin & Suchower Zt 추정 3. 거시연계 회귀모형 구축 4. 시나리오별 Zt 경로 생성 5. 50년 Lifetime PD 산출 6. 통계적 검증 7. 시각화 및 리포트 사용법: python main.py python main.py --horizon 30 python main.py --no-api # ECOS API 호출 없이 fallback 데이터 사용 python main.py --estimate-rho # 자산상관계수 동시 추정 """ import sys import io # Windows CP949 인코딩 문제 해결 if sys.stdout.encoding != 'utf-8': sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace') import argparse import logging import yaml import numpy as np import pandas as pd from pathlib import Path from tabulate import tabulate # 프로젝트 모듈 from data.transition_matrices import ( load_transition_matrices, compute_ttc_matrix, get_default_rates, display_matrix, RATING_GRADES, RATING_GRADES_8 ) from data.ccc_interpolator import expand_to_8x8 from data.macro_data import load_macro_data, _fallback_macro_data, compute_derived_features from models.credit_cycle import estimate_zt_series, estimate_rho_and_zt from models.vasicek import conditional_pd, worst_case_pd from models.macro_model import build_macro_zt_model from scenarios.scenario_engine import ScenarioEngine, load_config from projection.lifetime_pd import LifetimePDEngine, compute_ecl_weights from validation.statistical_tests import run_full_validation from output.visualizer import generate_all_plots # 로깅 설정 logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", datefmt="%H:%M:%S" ) logger = logging.getLogger(__name__) def parse_args(): parser = argparse.ArgumentParser(description="Lifetime PD (50년) 산출 시스템") parser.add_argument("--config", default="config.yaml", help="설정 파일 경로") parser.add_argument("--horizon", type=int, default=None, help="예측 기간 (기본: config 값)") parser.add_argument("--no-api", action="store_true", help="ECOS API 미사용 (fallback 데이터)") parser.add_argument("--estimate-rho", action="store_true", help="자산상관계수 동시 추정") parser.add_argument("--output", default=None, help="결과 저장 디렉토리") return parser.parse_args() def main(): args = parse_args() # ================================================================ # 0. 설정 로딩 # ================================================================ print("=" * 70) print(" Lifetime PD (50년) - 미래 경기 반영 부도율 산출 시스템") print("=" * 70) config = load_config(args.config) model_config = config.get("model", {}) rho = model_config.get("rho", 0.20) conv_config = config.get("convergence", {}) horizon = args.horizon or conv_config.get("total_horizon", 50) output_dir = args.output or config.get("output", {}).get("save_dir", "results") Path(output_dir).mkdir(parents=True, exist_ok=True) print(f"\n ρ (자산상관계수) = {rho}") print(f" 예측 기간 = {horizon}년") print(f" 결과 저장 = {output_dir}/") # ================================================================ # 1. 데이터 로딩 # ================================================================ print("\n" + "=" * 70) print(" [1/7] 데이터 로딩") print("=" * 70) # 전이행렬 data_config = config.get("data", {}) tm_source = data_config.get("transition_source", "builtin") tm_dir = data_config.get("transition_dir", None) logger.info(f"전이행렬 로딩 중 (source={tm_source})...") transition_matrices_all = load_transition_matrices(tm_source, data_dir=tm_dir) # 2000-2025 필터 transition_matrices_raw = {y:m for y,m in transition_matrices_all.items() if 2000 <= y <= 2025} # PD 플로어 적용: KAP 채권 YTM 기반 시장내재 PD from data.pd_floor import apply_pd_floor_to_matrices, build_complete_pd_floor_table pd_floors_broad, _, pd_floors_full = build_complete_pd_floor_table() transition_matrices = apply_pd_floor_to_matrices(transition_matrices_raw, pd_floors_broad) ttc_matrix = compute_ttc_matrix(transition_matrices) default_rates = get_default_rates(transition_matrices) print(f"\n 전이행렬: {len(transition_matrices)}개 연도 ({min(transition_matrices.keys())}~{max(transition_matrices.keys())})" f" [source={tm_source}]") print(f" PD 플로어 (KAP 채권 YTM 기반): AAA={pd_floors_broad['AAA']*10000:.0f}bp, AA={pd_floors_broad['AA']*10000:.0f}bp, " f"A={pd_floors_broad['A']*10000:.0f}bp, BBB={pd_floors_broad['BBB']*10000:.0f}bp") print(display_matrix(ttc_matrix, "TTC 전이행렬 (KAP PD Floor 적용 후 장기 평균)")) # 거시경제변수 if args.no_api: logger.info("Fallback 거시경제 데이터 사용") macro_data = _fallback_macro_data() # ECOS fallback 데이터도 병합 (37개 변수) try: from data.ecos_fetcher import load_macro_data as load_ecos_macro ecos_data = load_ecos_macro() if ecos_data is not None and not ecos_data.empty: macro_data = pd.concat([macro_data, ecos_data], axis=1) macro_data = macro_data.loc[:, ~macro_data.columns.duplicated()] logger.info(f"ECOS fallback 병합 완료: {len(macro_data.columns)}개 변수") except Exception as e: logger.warning(f"ECOS fallback 병합 실패: {e}") else: macro_data = load_macro_data(args.config) print(f"\n 거시변수: {len(macro_data)}개 연도, {len(macro_data.columns)}개 변수") print(f" 변수: {', '.join(macro_data.columns)}") print(macro_data.tail(5).to_string()) # 파생변수 계산 (회사채 로그수익률, 기간/신용스프레드) derived_features = compute_derived_features(macro_data) if not derived_features.empty: print(f"\n 파생변수: {', '.join(derived_features.columns)}") print(derived_features.tail(5).to_string()) # ================================================================ # 2. Belkin & Suchower Zt 추정 # ================================================================ print("\n" + "=" * 70) print(" [2/7] 신용사이클 인덱스 (Zt) 추정") print("=" * 70) if args.estimate_rho: logger.info("ρ와 Zt 동시 추정 중...") rho, zt_dict = estimate_rho_and_zt(transition_matrices, ttc_matrix) print(f"\n 추정된 ρ = {rho:.4f}") else: zt_dict = estimate_zt_series(transition_matrices, ttc_matrix, rho) zt_series = pd.Series(zt_dict, name="Zt") zt_series.index.name = "YEAR" print(f"\n Zt 통계: μ={zt_series.mean():.4f}, σ={zt_series.std():.4f}") print(f" 최소: {zt_series.min():.4f} ({zt_series.idxmin()})") print(f" 최대: {zt_series.max():.4f} ({zt_series.idxmax()})") zt_df = pd.DataFrame({"Year": zt_series.index, "Zt": zt_series.values}) print("\n" + tabulate(zt_df, headers="keys", tablefmt="simple", floatfmt=".4f")) # ================================================================ # 3. 거시연계 회귀모형 # ================================================================ print("\n" + "=" * 70) print(" [3/7] 거시연계 회귀모형 (Zt ~ 거시변수)") print("=" * 70) # 파생변수가 있으면 원본 + 파생 결합 if not derived_features.empty: model_input = pd.concat([macro_data, derived_features], axis=1) model_input = model_input.loc[:, ~model_input.columns.duplicated()] else: model_input = macro_data forced_vars = config.get("model", {}).get("macro_vars", None) macro_method = config.get("model", {}).get("macro_method", "ar1_macro") macro_model = build_macro_zt_model(zt_dict, model_input, method=macro_method, forced_vars=forced_vars) print(f"\n 선택된 변수: {macro_model.selected_vars}") if macro_model.is_ar1: import math phi = macro_model.ar1_phi half_life = math.log(2) / abs(math.log(abs(phi))) if 0 < abs(phi) < 1 else float('inf') print(f" [AR(1)+Macro] φ = {phi:.4f} (반감기 = {half_life:.1f}년)") print(f" c = {macro_model.ar1_const:.4f}") for var, beta in macro_model.ar1_beta.items(): print(f" β({var}) = {beta:+.6f}") print(macro_model.summary()) diag = macro_model.diagnostics() print(f"\n R² = {diag['r_squared']:.4f}") print(f" Adj. R² = {diag['adj_r_squared']:.4f}") print(f" AIC = {diag['aic']:.2f}") print(f" DW = {diag['durbin_watson']:.3f}") # ================================================================ # 4. 시나리오 생성 # ================================================================ print("\n" + "=" * 70) print(" [4/7] 시나리오 생성 (호황/중립/불황)") print("=" * 70) scenario_engine = ScenarioEngine(config) # 거시 시나리오 생성 macro_scenarios = scenario_engine.generate_default_macro_scenarios( macro_data, base_year=2025, forecast_years=5 ) # 시나리오에 파생변수 추가 (history + forecast로 lag/diff 계산) if not derived_features.empty: for sname, sdf in macro_scenarios.items(): # history + forecast 결합하여 파생변수 계산 combined = pd.concat([macro_data, sdf]) combined = combined[~combined.index.duplicated(keep='last')] combined = combined.sort_index() feat = compute_derived_features(combined) # forecast 연도만 추출 후 시나리오에 결합 forecast_years = sdf.index feat_forecast = feat.loc[feat.index.intersection(forecast_years)] if not feat_forecast.empty: macro_scenarios[sname] = pd.concat([sdf, feat_forecast], axis=1) macro_scenarios[sname] = macro_scenarios[sname].loc[:, ~macro_scenarios[sname].columns.duplicated()] # Zt 경로 생성 z_paths = scenario_engine.generate_z_paths( zt_dict, macro_model, macro_scenarios, base_year=2025 ) weights = scenario_engine.get_scenario_weights() print(f"\n 시나리오 가중치: {weights}") for name, path in z_paths.items(): display = scenario_engine.get_display_name(name) print(f"\n {display}:") print(f" Zt[1-5] = {path[:5].round(3)}") print(f" Zt[10] = {path[9]:.3f}") print(f" Zt[50] = {path[-1]:.3f}") # ================================================================ # 5. 50년 Lifetime PD 산출 # ================================================================ print("\n" + "=" * 70) print(" [5/7] 50년 Lifetime PD 산출") print("=" * 70) # 7x7 TTC -> 8x8 TTC (CCC interpolation) ttc_8x8 = expand_to_8x8(ttc_matrix) print(f"\n 7x7 TTC -> 8x8 TTC (CCC interpolated)") print(display_matrix(ttc_8x8, "TTC 8x8 (CCC interpolated)")) pd_engine = LifetimePDEngine(ttc_8x8, rho, rating_grades=RATING_GRADES_8) pd_results = pd_engine.compute_all_scenarios(z_paths, weights, horizon) # 누적 PD 테이블 print("\n === 가중평균 누적 PD (%) ===") cum_table = pd_engine.format_pd_table(pd_results) print(tabulate(cum_table * 100, headers="keys", tablefmt="simple", floatfmt=".3f")) # 시나리오별 주요 등급 비교 for scenario in z_paths.keys(): display = scenario_engine.get_display_name(scenario) print(f"\n === {display} 누적 PD (%) ===") s_table = pd_engine.format_pd_table(pd_results, scenario=scenario) print(tabulate(s_table * 100, headers="keys", tablefmt="simple", floatfmt=".3f")) # Vasicek Worst-Case 비교 print("\n === Basel II Worst-Case PD (99.9% VaR) ===") ttc_pds = ttc_8x8[:-1, -1] for i, grade in enumerate(RATING_GRADES_8[:-1]): wc = worst_case_pd(ttc_pds[i], rho) print(f" {grade}: TTC={ttc_pds[i]*100:.3f}% → WC={wc*100:.3f}%") # ================================================================ # 6. 통계적 검증 # ================================================================ print("\n" + "=" * 70) print(" [6/7] 통계적 검증") print("=" * 70) reg_result = macro_model.result if macro_model else None validation_df = run_full_validation( zt_series.values, reg_result, pd_results, list(RATING_GRADES[:-1]) ) print("\n" + tabulate(validation_df, headers="keys", tablefmt="grid")) # ================================================================ # 7. 시각화 # ================================================================ print("\n" + "=" * 70) print(" [7/7] 시각화 및 리포트 생성") print("=" * 70) generate_all_plots( zt_history=zt_dict, z_paths=z_paths, zt_series_pd=zt_series, macro_data=macro_data, pd_results=pd_results, ttc_matrix=ttc_matrix, validation_df=validation_df, output_dir=output_dir, base_year=2025 ) # ================================================================ # 결과 저장 (CSV) # ================================================================ out_path = Path(output_dir) # Zt 시계열 저장 zt_series.to_csv(out_path / "zt_series.csv") # 거시경제 데이터 저장 macro_data.to_csv(out_path / "macro_data.csv") # PD 테이블 저장 for scenario in list(z_paths.keys()) + [None]: label = scenario if scenario else "weighted" cum_df = pd_engine.format_pd_table( pd_results, years=list(range(1, horizon + 1)), scenario=scenario ) cum_df.to_csv(out_path / f"cumulative_pd_{label}.csv") marg_df = pd_engine.format_marginal_pd_table( pd_results, years=list(range(1, horizon + 1)), scenario=scenario ) marg_df.to_csv(out_path / f"marginal_pd_{label}.csv") # 검증 결과 저장 validation_df.to_csv(out_path / "validation_results.csv", index=False) # ================================================================ # 완료 # ================================================================ print("\n" + "=" * 70) print(" ✅ 완료!") print("=" * 70) print(f"\n 결과 파일:{output_dir}/") print(f" - 차트: 01~07_*.png") print(f" - 데이터: zt_series.csv, macro_data.csv") print(f" - PD: cumulative_pd_*.csv, marginal_pd_*.csv") print(f" - 검증: validation_results.csv") print() return 0 if __name__ == "__main__": sys.exit(main())