# -*- coding: utf-8 -*-
"""내 포트폴리오의 ENB(유효 베팅 수) 계산기

알고랩 「24종목을 샀는데 실질 베팅 수는 1.06이었습니다」 부록 · https://algolab.co.kr/enb

영상에서 쓴 것과 **같은 계산**입니다. 종목 코드만 넣으면 됩니다.

  pip install finance-datareader pandas numpy      # --csv 만 쓸 거면 pandas numpy 로 충분

  # 종목 코드 (등가중)
  python enb_calc.py 005930 000660 035420 051910 005380

  # 비중을 직접 주고 싶으면 코드:비중
  python enb_calc.py 005930:40 000660:30 035420:30

  # 기간 바꾸기
  python enb_calc.py 005930 000660 --start 2020-01-01

  # 직접 받은 종가 CSV로 (첫 열 날짜, 나머지 열이 종목)
  python enb_calc.py --csv my_prices.csv

무엇을 보는가
  ENC  = 비중만 보고 센 '유효 종목 수'. 등가중 N종목이면 N.
  ENB  = 위험 요인 기준 '유효 베팅 수'. **ENC보다 한참 작으면 종목 수만 늘린 것.**
  DR   = 분산화 비율. 개별 변동성 가중합 / 포트 변동성.

주의 — 영상에서도 말한 한계입니다.
  · ENB 1.06 은 "한 종목만 가지고 있다"는 뜻이 아닙니다.
    균등하게 분산된 N개의 독립적인 베팅과 거리가 멀다는 뜻입니다.
  · 기간·종목 선정·요인 추출 방식에 따라 숫자는 달라집니다. 절대 기준이 아닙니다.
  · 과거 데이터로 잰 값이며, 미래의 분산 효과를 보장하지 않습니다.
  · 투자 권유가 아닙니다. 투자 판단과 결과의 책임은 본인에게 있습니다.
"""
from __future__ import annotations

import sys
import argparse

import numpy as np
import pandas as pd

TRADING_DAYS = 252          # 연율화. 24시간 거래(코인)면 --ann 365


def load_prices(codes, start, end):
    """FinanceDataReader 로 종가를 받아 하나의 표로 합친다."""
    try:
        import FinanceDataReader as fdr
    except ImportError:
        sys.exit("finance-datareader 가 필요합니다:  pip install finance-datareader\n"
                 "(또는 --csv 로 직접 받은 종가 파일을 주세요)")
    cols = {}
    for c in codes:
        df = fdr.DataReader(c, start, end)
        if df.empty:
            sys.exit("데이터를 못 받았습니다: %s" % c)
        cols[c] = df["Close"]
    return pd.DataFrame(cols).sort_index()


def log_returns(px, min_ratio=0.9):
    """로그수익률. 이력이 짧아 결측이 많은 종목은 뺀다(짧은 종목이 표를 다 깎아먹는다)."""
    r = np.log(px / px.shift(1))
    keep = r.columns[r.notna().sum() >= min_ratio * len(r)]
    dropped = [c for c in r.columns if c not in keep]
    if dropped:
        print("  ! 이력이 부족해 제외: %s" % ", ".join(map(str, dropped)))
    return r[keep].dropna()


def enb(cov, w):
    """Meucci ENB — 공분산 주성분으로 위험을 회전시킨 뒤 비중 분포의 엔트로피 지수.

    각 주성분이 포트폴리오 분산에서 차지하는 몫 p 에 대해 ENB = exp(-Σ p ln p).
    한 요인이 전부를 설명하면 1, 요인들이 고르게 나눠 가지면 커진다.
    """
    vals, vecs = np.linalg.eigh(cov)
    o = np.argsort(vals)[::-1]
    vals, vecs = vals[o], vecs[:, o]
    contrib = (vecs.T @ w) ** 2 * vals
    p = np.clip(contrib, 0, None)
    p = p / p.sum()
    nz = p[p > 1e-15]
    return float(np.exp(-(nz * np.log(nz)).sum())), p


def main():
    ap = argparse.ArgumentParser(
        description="내 포트폴리오의 유효 베팅 수(ENB)를 계산합니다.")
    ap.add_argument("holdings", nargs="*",
                    help="종목코드 또는 코드:비중  (예: 005930:40 000660:30)")
    ap.add_argument("--csv", default=None, help="종가 CSV(첫 열 날짜, 나머지 열이 종목)")
    ap.add_argument("--start", default="2015-01-01")
    ap.add_argument("--end", default=None)
    ap.add_argument("--ann", type=int, default=TRADING_DAYS,
                    help="연율화 일수. 주식 252(기본) · 코인 365")
    a = ap.parse_args()

    if a.csv:
        px = pd.read_csv(a.csv, index_col=0, encoding="utf-8-sig")
        px.index = pd.to_datetime(px.index)
        px = px.sort_index().loc[a.start:a.end]
        weights = None
    else:
        if not a.holdings:
            ap.error("종목 코드를 주거나 --csv 를 쓰세요.")
        codes, ws = [], []
        for h in a.holdings:
            code, _, wt = h.partition(":")
            codes.append(code)
            ws.append(float(wt) if wt else None)
        px = load_prices(codes, a.start, a.end)
        weights = None if any(w is None for w in ws) else ws

    r = log_returns(px)
    n = r.shape[1]
    if n < 2:
        sys.exit("종목이 2개 이상이어야 계산할 수 있습니다.")

    if weights is not None and len(weights) == len(px.columns):
        w = np.array([weights[list(px.columns).index(c)] for c in r.columns], float)
        w = w / w.sum()
    else:
        w = np.ones(n) / n                 # 등가중

    S = np.cov(r.values.T) * a.ann         # 연율화 공분산
    C = np.corrcoef(r.values.T)
    sig = np.sqrt(np.diag(S))
    port = float(np.sqrt(w @ S @ w))
    rho = float(C[~np.eye(n, dtype=bool)].mean())

    enc = float(1.0 / (w ** 2).sum())      # 비중만 본 유효 종목 수
    dr = float((w @ sig) / port)           # 분산화 비율
    e, p = enb(S, w)

    print()
    print("  기간        {} ~ {}  ({}거래일)".format(
        r.index[0].date(), r.index[-1].date(), len(r)))
    print("  종목        {}개 — {}".format(n, ", ".join(map(str, r.columns))))
    print("  비중        {}".format("등가중" if weights is None else
                                    " · ".join("%.0f%%" % (x * 100) for x in w)))
    print("  " + "-" * 58)
    print("  평균 상관계수          {:6.2f}".format(rho))
    print("  개별 연변동성 평균     {:6.1f}%".format(sig.mean() * 100))
    print("  포트 연변동성          {:6.1f}%".format(port * 100))
    print("  " + "-" * 58)
    print("  ENC (비중만)           {:6.2f}".format(enc))
    print("  DR  (분산화 비율)      {:6.2f}".format(dr))
    print("  ENB (유효 베팅 수)     {:6.2f}   <-- 이 숫자".format(e))
    print("  최대 요인의 위험 비중  {:6.1f}%".format(p[0] * 100))
    print()
    print("  종목은 {}개인데 위험 기준 유효 베팅 수는 {:.2f}입니다.".format(n, e))
    print("  ENB 가 종목 수보다 한참 작다면, 늘린 것은 종목 수이지 위험의 원천이 아닙니다.")
    print("  ※ 절대 기준이 아닙니다. 기간·종목·계산 방식에 따라 달라집니다.")
    print()


if __name__ == "__main__":
    main()
