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:
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user