"""
================================================================================
확률 가중평균 시나리오 가중치 실증 분석
================================================================================

[목적]
IFRS 9 ECL 산출 시 복수 시나리오(호황/중립/불황)의 확률 가중치를 실증 데이터
기반으로 도출한다. 기존 하드코딩(20/50/30)을 대체할 객관적 가중치를 산출하고,
그 적정성을 통계적으로 검증한다.

[방법론 개요]

1. 시나리오 비율 선정: Downside 30% / Baseline 50% / Upside 20%

   (1) 30/50/20 비율의 근거
       - IFRS 9은 ECL을 "편향 없는(unbiased) 확률가중 금액"으로 측정하도록
         요구하며(5.5.17), 복수 시나리오의 확률가중을 통해 산출한다.
       - IFRS 9은 특정 시나리오 개수나 가중치 비율을 강제하지 않으므로,
         각 기관이 자체 근거를 문서화하여 결정해야 한다.
       - 30/50/20 분할은 다음 논리에 기반한다:
         a) Downside에 Upside보다 더 큰 비중(30% > 20%)을 배분하는 것은
            신용 손실 함수의 비선형성(convexity)을 반영한다.
            -> PD는 경기 악화 시 급증하고 호황 시에는 완만히 감소한다.
            -> 따라서 Downside 시나리오의 ECL 기여분이 Upside보다 크므로,
               대칭 가중(25/50/25)보다 불황 쪽에 더 큰 비중을 두는 것이
               보수적 추정(prudent estimation) 원칙에 부합한다.
         b) Baseline이 50%로 가장 큰 비중을 차지하는 것은,
            "가장 가능성 높은 시나리오"에 절반의 확률을 배정하는 것으로
            합리적인 중심 시나리오 설정이다.
         c) 불황(30%)과 호황(20%)의 합이 50%로, 양방향 테일을 균형있게
            반영하되 하방 리스크에 더 무게를 둔다.

   (2) 분위수에서의 의미
       - 30/50/20은 연속 확률분포상의 영역 분할으로 해석할 수 있다:
           Downside: 0 ~ 30th percentile  (하위 30%)
           Baseline: 30th ~ 80th percentile (중간 50%)
           Upside:   80th ~ 100th percentile (상위 20%)
       - 30th percentile과 80th percentile이 경계가 된다.

2. 가상 Zt(Virtual Zt) 구성

   (1) 월간 거시경제변수 3종의 12개월 롤링 연환산:
       - 원유(WTI): log(price) -- 수준(level) 변수
       - 순상품교역조건지수: 12개월 수익률 (T(t)/T(t-12) - 1)
       - KOSPI 평균: 12개월 log 수익률 (ln(T(t)/T(t-12)))

   (2) 12개월 롤링으로 변환하는 이유:
       - AR(1) 거시연계 모형의 beta 계수는 연간 데이터에서 추정되었으므로,
         동일한 단위(연간 수익률)로 변환해야 beta를 적용할 수 있다.
       - 월간 데이터를 그대로 사용하면 주기와 스케일이 맞지 않아
         beta 적용이 불가능하다.

   (3) beta 계수 적용:
       - 연간 Zt ~ 거시변수 회귀에서 추정된 beta를 사용한다.
       - Virtual_Zt(t) = const + b1 * X1(t) + b2 * X2(t) + b3 * X3(t)
       - 이렇게 하면 월별로 하나의 Virtual Zt값이 생성된다.
       - 300개월의 Virtual Zt -> 시나리오 비중 분석에 충분한 표본.

3. 정규분포 기반 확률 가중치 산출

   (1) 경험적 분위수로 경계값 산출:
       - q30 = Virtual Zt의 30th percentile 값
       - q80 = Virtual Zt의 80th percentile 값
       - 이 경계값은 30/50/20 분할에서 유래한다.

   (2) Virtual Zt에 정규분포 N(mu, sigma)를 피팅:
       - 정규분포 가정 하에, 경험적 분위수 경계의 "이론적" 면적을 산출한다.
       - 데이터가 완벽한 정규분포이면 면적은 정확히 30/50/20이 된다.
       - 실제 데이터의 비대칭성(skewness)이나 꼬리 두께(kurtosis)로 인해
         경험적 분위수의 위치가 정규분포상의 이론적 위치와 다르면,
         면적이 30/50/20에서 이탈한다.
       - 이 이탈(gap)이 "실증 데이터 기반 시나리오 가중치"이다.

   (3) 정규분포 가정의 적정성:
       - Jarque-Bera 정규성 검정으로 가정의 유효성을 확인한다.
       - 기각되지 않으면(p > 0.05) 정규분포 기반 면적을 가중치로 사용한다.
       - 기각되면 경험적 빈도(= 정의상 30/50/20)를 유지하거나,
         비모수적 방법(KDE 등)을 검토한다.

4. 결과 해석

   - 정규분포 면적이 30/50/20에서 이탈한 정도가 곧
     "실측 데이터에 기반한 보정된 시나리오 가중치"이다.
   - 예: 정규분포 면적이 27/56/17이면, 실제 데이터의 분포 특성상
     하드코딩 30/50/20보다 중립 비중이 높고 테일 비중이 낮다는 의미이다.
   - 이 가중치는 분기/연간으로 갱신 가능하며,
     감사(audit) 시 "과거 N개월 거시데이터의 분포에서 도출"이라는
     정량적 근거를 제시할 수 있다.

================================================================================
"""

import sys, io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')

import numpy as np
from scipy.stats import norm, jarque_bera


def analyze_scenario_weights(
    data_lists,
    betas,
    const,
    var_names=None,
    pct_down=30,
    pct_up=80,
):
    """
    확률 가중평균 시나리오 가중치 실증 분석

    Parameters
    ----------
    data_lists : list of array-like
        3개 거시변수의 변환된 시계열 데이터 (동일 길이).
        예: [oil_log, tot_ann_ret, kospi_ann_logret]
        각 리스트는 12개월 롤링 연환산 등 이미 변환 완료된 상태여야 한다.

    betas : list of float
        3개 거시변수의 회귀 계수.
        예: [-0.8037, -10.8856, 3.2878]

    const : float
        회귀 상수항.
        예: 2.8575

    var_names : list of str, optional
        변수 이름 (출력용). 기본값: ["X1", "X2", "X3"]

    pct_down : float
        Downside/Baseline 경계 분위수 (기본: 30)

    pct_up : float
        Baseline/Upside 경계 분위수 (기본: 80)

    Returns
    -------
    dict
        분석 결과를 담은 딕셔너리:
        - 'virtual_zt': Virtual Zt 배열
        - 'weights_empirical': 경험적 가중치 {'down': ..., 'base': ..., 'up': ...}
        - 'weights_normal': 정규분포 면적 기반 가중치
        - 'scenario_zt': 시나리오별 평균 Zt
        - 'normality_pvalue': Jarque-Bera p-value
        - 'q_boundaries': (q_low, q_high) 분위수 경계값
    """
    if var_names is None:
        var_names = ["X1", "X2", "X3"]

    # ========================================================================
    # 1. Virtual Zt 생성
    #    - beta 계수를 각 변수에 적용하여 월별 가상 Zt를 산출
    # ========================================================================
    X = np.column_stack(data_lists)
    beta_arr = np.array(betas)
    Z_v = const + X @ beta_arr

    n = len(Z_v)
    print(f'=== Virtual Zt ({n} observations) ===')
    print(f'mu    = {Z_v.mean():.4f}')
    print(f'sigma = {Z_v.std():.4f}')
    print(f'min   = {Z_v.min():.4f}')
    print(f'max   = {Z_v.max():.4f}')
    print()

    # Sanity check: top/bottom 5
    sorted_idx = np.argsort(Z_v)
    print('Bottom 5 (recession):')
    for i in sorted_idx[:5]:
        parts = '  '.join(f'{var_names[j]}={X[i,j]:.4f}' for j in range(3))
        print(f'  [{i:3d}] Zt={Z_v[i]:+.4f}  {parts}')
    print('Top 5 (boom):')
    for i in sorted_idx[-5:][::-1]:
        parts = '  '.join(f'{var_names[j]}={X[i,j]:.4f}' for j in range(3))
        print(f'  [{i:3d}] Zt={Z_v[i]:+.4f}  {parts}')
    print()

    # ========================================================================
    # 2. 분위수 경계 설정
    # ========================================================================
    q_low = np.percentile(Z_v, pct_down)
    q_high = np.percentile(Z_v, pct_up)

    pct_base = pct_up - pct_down
    pct_up_width = 100 - pct_up

    print(f'=== Percentile boundaries ({pct_down}/{pct_base}/{pct_up_width}) ===')
    print(f'q{pct_down} = {q_low:.4f}  (downside/baseline)')
    print(f'q{pct_up} = {q_high:.4f}  (baseline/upside)')
    print()

    # ========================================================================
    # 3. 경험적 빈도 (정의상 pct_down / pct_base / pct_up_width)
    # ========================================================================
    w_d = np.mean(Z_v < q_low)
    w_m = np.mean((Z_v >= q_low) & (Z_v <= q_high))
    w_u = np.mean(Z_v > q_high)

    print(f'=== Empirical frequency (sanity check) ===')
    print(f'Downside: {w_d*100:.1f}%')
    print(f'Baseline: {w_m*100:.1f}%')
    print(f'Upside:   {w_u*100:.1f}%')
    print()

    # ========================================================================
    # 4. 정규분포 피팅 -> 면적 산출 (실증 가중치)
    # ========================================================================
    mu_z = Z_v.mean()
    sig_z = Z_v.std()
    p_d = norm.cdf(q_low, mu_z, sig_z)
    p_u = 1 - norm.cdf(q_high, mu_z, sig_z)
    p_m = 1 - p_d - p_u

    print(f'=== Normal N({mu_z:.4f}, {sig_z:.4f}) area ===')
    print(f'Downside (Z < {q_low:.4f}): {p_d*100:.2f}%')
    print(f'Baseline ({q_low:.4f} ~ {q_high:.4f}): {p_m*100:.2f}%')
    print(f'Upside   (Z > {q_high:.4f}): {p_u*100:.2f}%')
    print()

    print(f'=== KEY RESULT ===')
    print(f'Downside: empirical {pct_down:.0f}% vs normal {p_d*100:.2f}%')
    print(f'Baseline: empirical {pct_base:.0f}% vs normal {p_m*100:.2f}%')
    print(f'Upside:   empirical {pct_up_width:.0f}% vs normal {p_u*100:.2f}%')
    print()

    # ========================================================================
    # 5. 시나리오별 대표 Zt
    # ========================================================================
    z_down = Z_v[Z_v < q_low].mean()
    z_base = Z_v[(Z_v >= q_low) & (Z_v <= q_high)].mean()
    z_up = Z_v[Z_v > q_high].mean()

    print(f'=== Scenario Zt levels ===')
    print(f'Downside avg Zt = {z_down:.4f}')
    print(f'Baseline avg Zt = {z_base:.4f}')
    print(f'Upside   avg Zt = {z_up:.4f}')
    print()

    # ========================================================================
    # 6. 정규성 검정 (Jarque-Bera)
    # ========================================================================
    jb_stat, jb_p = jarque_bera(Z_v)
    print(f'=== Normality test (Jarque-Bera) ===')
    print(f'JB stat = {jb_stat:.2f}, p-value = {jb_p:.4f}')
    if jb_p < 0.05:
        print('-> REJECT normality (p < 0.05) -> empirical quantile preferred')
    else:
        print('-> Cannot reject normality -> normal assumption OK')
    print()

    return {
        'virtual_zt': Z_v,
        'weights_empirical': {
            'down': w_d, 'base': w_m, 'up': w_u
        },
        'weights_normal': {
            'down': p_d, 'base': p_m, 'up': p_u
        },
        'scenario_zt': {
            'down': z_down, 'base': z_base, 'up': z_up
        },
        'normality_pvalue': jb_p,
        'q_boundaries': (q_low, q_high),
    }


# ============================================================================
# 사용 예시
# ============================================================================
if __name__ == '__main__':
    import pandas as pd
    import warnings
    warnings.filterwarnings('ignore')

    # --- 데이터 로딩 ---
    df = pd.read_excel(
        r'C:\Users\Certes\Downloads\ECOS데이터조회(월기준)_20260317_3개_수정본.xlsx',
        sheet_name='데이터'
    )
    cols = df.columns.tolist()
    item_col = cols[2]
    month_cols = [c for c in cols if '/' in str(c)]

    data = {}
    for _, row in df.iterrows():
        name = str(row[item_col]).strip()
        vals = pd.Series(row[month_cols].values.astype(float), index=month_cols)
        data[name] = vals

    keys = list(data.keys())
    oil = data[[k for k in keys if 'WTI' in k][0]]
    tot = data[[k for k in keys if 'KOSPI' not in k and 'WTI' not in k][0]]
    kospi = data[[k for k in keys if 'KOSPI' in k][0]]

    # --- 12개월 롤링 연환산 변환 ---
    oil_log = np.log(oil)
    tot_ann_ret = tot / tot.shift(12) - 1
    kospi_ann_logret = np.log(kospi / kospi.shift(12))

    valid = oil_log.notna() & tot_ann_ret.notna() & kospi_ann_logret.notna()
    months = np.array(month_cols)[valid.values]
    print(f'Period: {months[0]} ~ {months[-1]} ({len(months)} months)')
    print()

    # --- 분석 실행 ---
    result = analyze_scenario_weights(
        data_lists=[
            oil_log[valid].values,
            tot_ann_ret[valid].values,
            kospi_ann_logret[valid].values,
        ],
        betas=[-0.8037, -10.8856, 3.2878],
        const=2.8575,
        var_names=['oil_log', 'tot_ret', 'kospi_logret'],
        pct_down=30,
        pct_up=80,
    )
