"""
가격 데이터를 받아 로컬에 저장해 두고, 받을 때마다 점검하는 작은 도구

사용법:
    pip install pandas finance-datareader
    python data_cache.py KS11            # 받아서 점검하고 data_cache/ 에 저장
    python data_cache.py KS11 --offline  # 저장해 둔 사본만 사용

하는 일:
  1. 새로 받은 데이터와 저장해 둔 사본을 겹치는 날짜끼리 비교해, 과거 값이 바뀌었으면 알린다.
  2. 빈 데이터, 날짜 중복, 열흘 넘는 공백, 하루 ±30%를 넘는 변동을 점검한다.
  3. 받기에 실패하면 저장해 둔 사본으로 대신하고 그 사실을 알린다.
  4. 데이터 기간과 '지문'(내용 해시)을 출력해, 결과 옆에 같이 적어 둘 수 있게 한다.
  5. 월말 종가가 필요하면 끝나지 않은 마지막 달을 빼고 돌려준다.
"""
import argparse
import hashlib
import os

import pandas as pd

CACHE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data_cache")


def fingerprint(s):
    """데이터 내용이 같으면 같은 문자열. 결과표 옆에 적어 두면 나중에 같은 데이터인지 확인할 수 있다."""
    return hashlib.sha1(s.round(6).to_csv().encode()).hexdigest()[:10]


def check(s, max_gap_days=10, max_move=0.30):
    problems = []
    if s.empty:
        return ["데이터가 비어 있음"]
    if s.index.duplicated().any():
        problems.append(f"날짜 중복 {int(s.index.duplicated().sum())}건")
    if not s.index.is_monotonic_increasing:
        problems.append("날짜 순서가 뒤섞임")
    gaps = s.index.to_series().diff().dt.days
    for d, g in gaps[gaps > max_gap_days].items():
        problems.append(f"{d.date()} 앞에 {int(g)}일 공백")
    r = s.pct_change().dropna()
    for d, v in r[r.abs() > max_move].items():
        problems.append(f"{d.date()} 하루 변동 {v:+.1%}")
    return problems


def load(ticker, start="1995-01-01", offline=False):
    os.makedirs(CACHE, exist_ok=True)
    path = os.path.join(CACHE, f"{ticker}.csv")
    cached = pd.read_csv(path, index_col=0, parse_dates=True).iloc[:, 0] if os.path.exists(path) else None
    fresh = None
    if not offline:
        try:
            import FinanceDataReader as fdr
            fresh = fdr.DataReader(ticker, start)["Close"].dropna()
            if fresh.empty:
                fresh = None
                print("경고: 빈 데이터를 받았습니다.")
        except Exception as e:                       # 네트워크 오류, 응답 형식 변경 등
            print(f"경고: 새로 받지 못했습니다 ({type(e).__name__}).")
    if fresh is None and cached is None:
        raise RuntimeError("새로 받지도 못했고 저장해 둔 사본도 없습니다.")
    if fresh is not None and cached is not None:
        common = cached.index.intersection(fresh.index)
        diff = (fresh[common] / cached[common] - 1).abs()
        n = int((diff > 1e-6).sum())
        if n:
            print(f"경고: 이미 받아 둔 {len(common)}일 중 {n}일의 값이 바뀌었습니다(최대 {diff.max():.2%}).")
        missing = cached.index.difference(fresh.index)
        if len(missing):
            print(f"경고: 예전에 있던 {len(missing)}일이 새 데이터에서 빠졌습니다.")
    s = fresh if fresh is not None else cached
    if fresh is None:
        print("저장해 둔 사본으로 계속합니다.")
    for p in check(s):
        print("점검:", p)
    if fresh is not None:
        s.rename("Close").to_csv(path)
        s.rename("Close").to_csv(os.path.join(CACHE, f"{ticker}_{s.index[-1]:%Y%m%d}.csv"))   # 날짜별 사본도 남긴다
    print(f"{ticker}: {s.index[0].date()} ~ {s.index[-1].date()}, {len(s)}행, 지문 {fingerprint(s)}")
    return s


def month_end(s):
    """월말 종가. 마지막 달이 아직 끝나지 않았으면 그 달은 뺀다.
    한국 휴장일(예: 12월 31일)은 모르므로, 데이터가 그 직전 거래일에 끝나면 끝난 달도 뺄 수 있다(보수적으로 틀림)."""
    m = s.resample("ME").last().dropna()
    last = s.index[-1]
    if last.month == (last + pd.offsets.BDay(1)).month:     # 다음 영업일이 같은 달이면 아직 월말이 아니다
        m = m.iloc[:-1]
    return m


if __name__ == "__main__":
    ap = argparse.ArgumentParser()
    ap.add_argument("ticker", nargs="?", default="KS11")
    ap.add_argument("--offline", action="store_true")
    a = ap.parse_args()
    s = load(a.ticker, offline=a.offline)
    m = month_end(s)
    print(f"월말 종가 {len(m)}개월, 마지막 {m.index[-1].date()}")
