feat(model): KAP YTM PD floor integration, expanded 226-var search, ADF fix (AIC->BIC), Model#2 with 6-test diagnostics

- Replace hardcoded DEFAULT_PD_FLOORS with build_complete_pd_floor_table() (KAP bond YTM)
- Fix ADF test: autolag='AIC' -> 'BIC' for small sample (N=26) robustness
- Expand variable search: 40 -> 226 vars (log/diff/return/lag2), 1.9M combos
- Select Model #2: HOUSING_PRICE + CREDIT_SPREAD_LAG1 + CURRENT_ACCOUNT_R
- Add 6-test diagnostics table to AR1 sheet (ADF/LB/DW/BP/ARCH/Shapiro)
- Add Korean variable names for transformed variables
- Generate report v7 with full diagnostics
This commit is contained in:
Variet Agent
2026-03-12 00:06:23 +09:00
parent 87725b7c19
commit d1ddf06e5d
19 changed files with 1736 additions and 167 deletions

View File

@@ -58,14 +58,17 @@ class ScenarioEngine:
"""
시나리오별 Zt 경로 생성 (50년)
AR(1) 모형이 있으면: forecast_z_path() 사용 (macro_shocks 기반)
없으면: Z-직접 방식 (μ±kσ) fallback
Parameters
----------
zt_history : Dict[int, float]
과거 Zt 시계열
macro_model : MacroZtModel, optional
거시→Zt 회귀모형
거시→Zt 회귀모형 (AR(1) 또는 OLS)
macro_scenarios : Dict[str, pd.DataFrame], optional
시나리오별 거시변수 경로
(OLS 모형용) 시나리오별 거시변수 경로
base_year : int
기준 연도
@@ -77,55 +80,83 @@ class ScenarioEngine:
zt_values = np.array(list(zt_history.values()))
z_mean = zt_values.mean()
z_std = zt_values.std()
z_last = zt_values[-1] # 마지막 관측값
logger.info(f"Zt 통계: μ={z_mean:.4f}, σ={z_std:.4f}")
logger.info(f"Zt 통계: μ={z_mean:.4f}, σ={z_std:.4f}, Z_last={z_last:.4f}")
z_paths = {}
for scenario_name, scenario_cfg in self.scenario_config.items():
z_multiplier = scenario_cfg.get("z_multiplier", 0.0)
# AR(1) 모형 사용 경로
use_ar1 = (macro_model is not None and
hasattr(macro_model, 'is_ar1') and
macro_model.is_ar1)
if use_ar1:
logger.info("AR(1)+Macro 모형으로 시나리오 경로 생성")
# 시나리오별 초기 Z 수준
z_scenario = z_mean + z_multiplier * z_std
for scenario_name, scenario_cfg in self.scenario_config.items():
# config에서 macro_shocks 가져오기
macro_shocks = scenario_cfg.get("macro_shocks", {})
# forecast_z_path로 50년 경로 생성
z_path = macro_model.forecast_z_path(
z_last=z_last,
macro_shocks=macro_shocks,
horizon=self.total_horizon
)
z_paths[scenario_name] = z_path
logger.info(
f" {scenario_name}: Z[1]={z_path[0]:+.3f}, "
f"Z[5]={z_path[4]:+.3f}, Z[10]={z_path[9]:+.3f}, "
f"Z[50]={z_path[-1]:+.3f}"
)
else:
# Fallback: Z-직접 방식
logger.info("Z-직접 방식으로 시나리오 경로 생성 (AR(1) 없음)")
# 거시 모형이 있으면 단기(1-5년) 거시 기반 Zt 예측
if macro_model is not None and macro_scenarios is not None:
scenario_key = scenario_name
if scenario_key in macro_scenarios:
macro_path = macro_scenarios[scenario_key]
z_short = macro_model.predict(macro_path)
n_short = min(len(z_short), self.pit_horizon)
for scenario_name, scenario_cfg in self.scenario_config.items():
z_multiplier = scenario_cfg.get("z_multiplier", 0.0)
z_scenario = z_mean + z_multiplier * z_std
# OLS 거시 모형이 있으면 단기 거시 기반
if macro_model is not None and macro_scenarios is not None:
scenario_key = scenario_name
if scenario_key in macro_scenarios:
macro_path = macro_scenarios[scenario_key]
z_short = macro_model.predict(macro_path)
n_short = min(len(z_short), self.pit_horizon)
else:
z_short = np.full(self.pit_horizon, z_scenario)
n_short = self.pit_horizon
else:
z_short = np.full(self.pit_horizon, z_scenario)
n_short = self.pit_horizon
else:
z_short = np.full(self.pit_horizon, z_scenario)
n_short = self.pit_horizon
# 전체 50년 Zt 경로 구성
z_path = np.zeros(self.total_horizon)
# Phase 1: PIT 기간 (1~pit_horizon년)
for t in range(min(n_short, self.total_horizon)):
val = z_short[t] if t < len(z_short) else z_scenario
z_path[t] = val if np.isfinite(val) else z_scenario
# Phase 2: Mean-reversion 기간 (pit_horizon+1 ~ transition_horizon년)
for t in range(self.pit_horizon, min(self.transition_horizon, self.total_horizon)):
decay = np.exp(-self.mean_reversion_lambda * (t - self.pit_horizon + 1))
z_path[t] = z_path[self.pit_horizon - 1] * decay
# Phase 3: TTC 기간 (transition_horizon+1 ~ total_horizon년)
for t in range(self.transition_horizon, self.total_horizon):
z_path[t] = 0.0 # TTC (Z=0)
z_paths[scenario_name] = z_path
logger.info(
f" {scenario_name}: Z[1]={z_path[0]:+.3f}, "
f"Z[5]={z_path[4]:+.3f}, Z[10]={z_path[9]:+.3f}, "
f"Z[50]={z_path[-1]:+.3f}"
)
z_path = np.zeros(self.total_horizon)
# Phase 1: PIT 기간
for t in range(min(n_short, self.total_horizon)):
val = z_short[t] if t < len(z_short) else z_scenario
z_path[t] = val if np.isfinite(val) else z_scenario
# Phase 2: Mean-reversion
for t in range(self.pit_horizon, min(self.transition_horizon, self.total_horizon)):
decay = np.exp(-self.mean_reversion_lambda * (t - self.pit_horizon + 1))
z_path[t] = z_path[self.pit_horizon - 1] * decay
# Phase 3: TTC
for t in range(self.transition_horizon, self.total_horizon):
z_path[t] = 0.0
z_paths[scenario_name] = z_path
logger.info(
f" {scenario_name}: Z[1]={z_path[0]:+.3f}, "
f"Z[5]={z_path[4]:+.3f}, Z[10]={z_path[9]:+.3f}, "
f"Z[50]={z_path[-1]:+.3f}"
)
return z_paths