# -*- coding: utf-8 -*-
"""연휴 앞 코스피 — 팔고 갈까, 들고 갈까 (AlgoLab · 2026-10-30)

표준 라이브러리만 씁니다. 같은 폴더의 가격 자료(CSV)를 읽어 영상·설명란의 숫자를 전부 다시 냅니다.
    python run_holiday.py

규칙
- 휴장일: 평일인데 가격이 없는 날. 코스피 지수는 코스닥 지수에도 가격이 없어야 휴장으로 셉니다.
- 긴 연휴: 연휴 앞 마지막 거래일과 연휴 뒤 첫 거래일 사이가 달력으로 4일 이상(주말 포함 사흘 이상 쉼).
  사이에 빈 평일이 6일을 넘으면 자료 구멍으로 보고 뺍니다.
- 팔고 가는 계좌: 연휴 앞 마지막 거래일 종가에 전부 팔고, 연휴 뒤 첫 거래일 종가에 다시 삽니다(쉬는 동안 이자 0).
- 들고 가는 계좌: 처음에 사서 끝까지 보유.
- 수수료: 편도 0.02%(수수료+호가 차이 근사). 0 · 0.1% 도 같이 냅니다.
- 시작: 1999-01-04. 국내 증시는 1998년 12월 전까지 토요일에도 장이 열렸는데 가격 자료에 토요일 봉이 없습니다.
  그 시절 '금요일 휴장'은 사흘 쉼이 아니었을 수 있어 연휴 판정을 믿을 수 없으므로 1999년부터 셉니다.
자료 끝: 코스피·코스닥 지수 2026-10-29 · KODEX 200(069500, 분배금 반영 가격) 2026-10-20
"""
import csv
import os
from datetime import date, timedelta

HERE = os.path.dirname(os.path.abspath(__file__))
ASOF = {'KS11': '2026-10-29', 'KQ11': '2026-10-29', '069500': '2026-10-20'}
START = '1999-01-01'


def load(name, cols=('close',)):
    out = []
    with open(os.path.join(HERE, name + '_1d.csv'), encoding='utf-8') as f:
        for r in csv.DictReader(f):
            if r['date'] > ASOF[name]:
                continue
            try:
                v = tuple(float(r[c]) for c in cols)
            except ValueError:
                continue
            if all(x > 0 for x in v):
                out.append((r['date'],) + v)
    return out


def events(s, other_dates=()):
    """거래일 i-1 → i 간격의 종류: long(긴 연휴) · weekend · day · other · hole"""
    out = []
    for i in range(1, len(s)):
        a, b = date.fromisoformat(s[i - 1][0]), date.fromisoformat(s[i][0])
        gap = (b - a).days
        miss = sum(1 for k in range(1, gap) if (a + timedelta(days=k)).weekday() < 5)
        if miss > 6:
            kind = 'hole'
        elif gap >= 4:
            kind = 'hole' if any((a + timedelta(days=k)).isoformat() in od for k in range(1, gap) for od in other_dates) else 'long'
        elif gap == 3 and a.weekday() == 4:
            kind = 'weekend'
        else:
            kind = 'day' if gap == 1 else 'other'
        out.append((i, kind))
    return out


def account(s, ev, sell, cost):
    eq = 1.0
    for i, k in ev:
        eq *= (1 - cost) ** 2 if sell(i, k) else s[i][1] / s[i - 1][1]
    return round(1000 * eq)  # 1,000만원 → 만원


def man(x):
    return f'{x:,}만원'


def main():
    C = 0.0002
    ks = [x for x in load('KS11') if x[0] >= START]
    kq = set(d for d, *_ in load('KQ11'))
    ev = events(ks, [kq])
    L = [i for i, k in ev if k == 'long']
    ret = lambda i: ks[i][1] / ks[i - 1][1] - 1
    hold = lambda i, k: False
    sell = lambda i, k: k == 'long'
    print(f'코스피 지수 {ks[0][0]} ~ {ks[-1][0]} · 긴 연휴 {len(L)}번')
    for c in (0, C, 0.001):
        print(f'  편도 {c*100:.2f}%: 들고 가기 {man(account(ks, ev, hold, c))} · 팔고 가기 {man(account(ks, ev, sell, c))}')
    up = sum(1 for i in L if ret(i) > 0)
    print(f'  연휴 뒤 올라 있던 때 {up}/{len(L)}'
          f' · 1999~2011 {sum(1 for i in L if ks[i][0] < "2012" and ret(i) > 0)}/{sum(1 for i in L if ks[i][0] < "2012")}'
          f' · 2012~ {sum(1 for i in L if ks[i][0] >= "2012" and ret(i) > 0)}/{sum(1 for i in L if ks[i][0] >= "2012")}')
    top = sorted(L, key=ret, reverse=True)[:3]; worst = min(L, key=ret)
    print('  가장 크게 오른 연휴 ' + ' · '.join(f'{ks[i-1][0]} → {ks[i][0]} {100*ret(i):+.2f}%' for i in top) + f' / 가장 크게 내린 연휴 {ks[worst-1][0]} → {ks[worst][0]} {100*ret(worst):+.2f}%')
    # 해마다
    yr = {}
    for i, k in ev:
        if k == 'hole':
            continue
        y = ks[i][0][:4]; r = ks[i][1] / ks[i - 1][1]
        h, v, n, s = yr.get(y, (1.0, 1.0, 0, 0.0))
        yr[y] = (h * r, v * ((1 - C) ** 2 if k == 'long' else r), n + (k == 'long'), s + (r - 1 if k == 'long' else 0))
    won = [y for y, (h, v, n, s) in sorted(yr.items()) if n and v > h]
    print(f'  팔고 간 쪽이 앞선 해 {len(won)}/{sum(1 for y in yr.values() if y[2])}: {" ".join(won)}')
    sums = sorted(((100 * s, y) for y, (h, v, n, s) in yr.items() if n))
    print('  연휴 건너기 합이 가장 나빴던 해: ' + ' · '.join(f'{y} {s:+.1f}%' for s, y in sums[:3]))
    y26 = [i for i in L if ks[i][0][:4] == '2026']
    print(f'  2026년 긴 연휴 {len(y26)}번 중 내림 {sum(1 for i in y26 if ret(i) < 0)}번: '
          + ' · '.join(f'{ks[i-1][0][5:]}→{ks[i][0][5:]} {100*ret(i):+.2f}%' for i in y26))
    # 팔고 쉬는 동안 예금 이자를 주면(쉰 달력일 × 고정 연 이율)
    days = sum((date.fromisoformat(ks[i][0]) - date.fromisoformat(ks[i - 1][0])).days for i in L)
    row = []
    for rate in (0.03, 0.05, 0.10):
        v = 1.0
        for i, k in ev:
            if k == 'long':
                d = (date.fromisoformat(ks[i][0]) - date.fromisoformat(ks[i - 1][0])).days
                v *= (1 - C) ** 2 * (1 + rate) ** (d / 365)
            else:
                v *= ks[i][1] / ks[i - 1][1]
        row.append(f'연 {int(rate*100)}% {man(round(1000 * v))}')
    print(f'  팔고 쉬는 동안 이자를 주면(쉰 날 합 {days}일): ' + ' · '.join(row) + f' (들고 가기 {man(account(ks, ev, hold, C))})')
    for lo, hi, lab in (('2012', '2026', '2012~2025'), ('2012', '2027', '2012~2026-10')):
        e = [(i, k) for i, k in ev if lo <= ks[i][0] < hi]
        print(f'  {lab}: 들고 가기 {man(account(ks, e, hold, C))} · 팔고 가기 {man(account(ks, e, sell, C))}')
    # 실제 ETF
    kd = [x for x in load('069500', ('open', 'close')) if x[0] >= '2009-05-01']
    sc = [(d, c) for d, o, c in kd]
    e2 = events(sc, [set(d for d, *_ in ks)])
    print(f'\nKODEX 200(069500) {sc[0][0]} ~ {sc[-1][0]} · 긴 연휴 {sum(1 for _, k in e2 if k == "long")}번')
    for c in (0, C, 0.001):
        print(f'  편도 {c*100:.2f}%: 들고 가기 {man(account(sc, e2, hold, c))} · 팔고 가기 {man(account(sc, e2, sell, c))}')
    e3 = [(i, k) for i, k in e2 if sc[i][0] < '2026']
    print(f'  2025년까지(0.02%): 들고 가기 {man(account(sc, e3, hold, C))} · 팔고 가기 {man(account(sc, e3, sell, C))}')
    # 연휴 뒤 첫날 아침 시가에 다시 사기(편도 0.1%)
    eq = 1.0
    for i, k in e2:
        if k == 'long':
            eq *= (1 - 0.001) ** 2 * kd[i][2] / kd[i][1]   # 시가에 사서 그날 종가까지
        else:
            eq *= kd[i][2] / kd[i - 1][2]
    print(f'  덧붙임 — 연휴 뒤 첫날 시가에 다시 사기(편도 0.1%): {man(round(1000 * eq))}')


if __name__ == '__main__':
    main()
