# -*- coding: utf-8 -*-
"""물린 대형주 — 그냥 버틸까, 절반을 코스피200 ETF 로 옮길까 (AlgoLab 재현 코드 · 2026-10-17)

무엇을 재나
  2007년 12월 28일 코스피 시가총액 상위 40종목(보통주). 나중에 상장폐지·합병된 6종목도 넣었다
  (네이버 종가 + 합병 뒤 존속 회사 수익률로 이어 붙임 · 이 6종목 구간은 배당 미반영).
  2002년부터 매달 첫 거래일에 그 종목을 산다고 본다. 산 뒤 3년 안에 종가가 산 값의 70% 이하가 된 첫날 = 물린 날
  (물린 날이 2007-02-01 이후 · 같은 종목·같은 물린 날은 한 번만 센다).
  물린 날 계좌 700만원(산 값 1,000만원)에서 3년(750거래일) 뒤 금액을 세 가지로 본다.
    그냥 버티기 = 그대로 든다
    절반 옮기기 = 물린 날 종가에 절반을 팔아 KODEX 200 을 산다
    전부 옮기기 = 물린 날 종가에 전부 팔아 KODEX 200 을 산다
  옮기는 몫에는 매도 수수료 0.1% + 거래세 0.2% + 매수 수수료 0.1% = 0.4% 를 한 번 뺀다.
  양도세·배당 세금·호가 차이는 넣지 않았다. 종가에 사고판다고 쳤다.

자료(data_stuckbig/ · 2026-10-02 까지로 얼림)
  <종목코드>.csv  야후 수정 종가(상폐·합병 6종은 네이버 종가 이음) · KODEX200.csv (2002~ 이음) · KS11.csv (코스피 · 거래일 대조용)
  하루 등락이 ±31% 를 넘는 날은 자료 오류로 보고 0 으로 둔다.
  인적분할 날(야후 수정 종가가 떨어져 나간 회사 몫을 못 담는 날) 넷도 0 으로 둔다 — 아래 SPINOFF.

돌리는 법:  pip install numpy  →  python run_stuckbig.py
"""
import collections
import csv
import os

import numpy as np

HERE = os.path.dirname(os.path.abspath(__file__))
D = os.path.join(HERE, "data_stuckbig")
ASOF = "2026-10-02"
LOSS = 0.70          # 산 값의 70% = 30% 물림
H = 750              # 3년(거래일)
COST = 0.004         # 옮길 때 팔고 사는 비용(한 번)
DEV_END = "2015-12-31"
SPINOFF = {"023530": ["2017-10-30"], "003550": ["2021-05-27"], "017670": ["2021-11-29"], "012630": ["2018-06-12"]}
NAME = {"000210": "대림산업", "000660": "SK하이닉스", "000720": "현대건설", "000810": "삼성화재", "000830": "삼성물산(옛)",
        "003550": "LG", "003600": "SK(옛)", "004020": "현대제철", "004170": "신세계", "004940": "외환은행",
        "005380": "현대차", "005490": "POSCO", "005930": "삼성전자", "006360": "GS건설", "009540": "현대중공업",
        "010140": "삼성중공업", "010950": "S-Oil", "012330": "현대모비스", "012630": "현대산업개발", "015760": "한국전력",
        "016360": "삼성증권", "017670": "SK텔레콤", "023530": "롯데쇼핑", "024110": "기업은행", "028670": "STX팬오션",
        "029780": "삼성카드", "030200": "KT", "033780": "KT&G", "034020": "두산중공업", "034220": "LG디스플레이",
        "037620": "미래에셋증권(옛)", "042660": "대우조선해양", "047040": "대우건설", "051910": "LG화학", "053000": "우리금융(옛)",
        "060000": "국민은행", "066570": "LG전자", "086790": "하나금융지주", "096770": "SK에너지"}


def load(path, clean=False, code=None):
    rows = [r for r in csv.reader(open(path, encoding="utf-8")) if r and r[0][:1].isdigit()]
    d = np.array([r[0] for r in rows]); c = np.array([float(r[4]) for r in rows])
    k = d <= ASOF
    d, c = d[k], c[k]
    if clean:
        k = c > 0
        d, c = d[k], c[k]
        r = c[1:] / c[:-1] - 1
        r[np.abs(r) > 0.31] = 0.0
        for day in SPINOFF.get(code, []):
            j = np.where(d[1:] == day)[0]
            assert len(j) == 1, (code, day)
            r[j[0]] = 0.0
        c = c[0] * np.concatenate([[1.0], np.cumprod(1 + r)])
    return d, c


kd, kc = load(os.path.join(D, "KODEX200.csv"))
xd, _ = load(os.path.join(D, "KS11.csv"))
kmap = {d: i for i, d in enumerate(kd)}
xset = set(xd)
codes = sorted(f[:-4] for f in os.listdir(D) if f[:1].isdigit() and f.endswith(".csv"))
assert len(codes) == 40, len(codes)
SER = {c: load(os.path.join(D, c + ".csv"), clean=True, code=c) for c in codes}


def events(loss=LOSS):
    out = []
    for code in codes:
        d, c = SER[code]
        seen, months = set(), {}
        for i, di in enumerate(d):
            if di >= "2002-01-01" and di[:7] not in months:
                months[di[:7]] = i
        for b in months.values():
            hit = np.where(c[b + 1:min(len(c), b + 750)] <= loss * c[b])[0]
            if not len(hit):
                continue
            t = b + 1 + hit[0]
            if d[t] < "2007-02-01" or (code, t) in seen:
                continue
            seen.add((code, t))
            if d[b] not in xset or d[t] not in xset or d[t] not in kmap:
                continue
            ki = kmap[d[t]]
            if not (t + H < len(c) and ki + H < len(kc)):
                continue
            if d[t + H] == kd[ki + H]:
                kj = ki + H
            elif d[t + H] in kmap:
                kj = kmap[d[t + H]]
            else:
                continue
            out.append({"code": code, "buy": d[b], "t": d[t], "end": d[t + H], "h": c[t + H] / c[t],
                        "s": kc[kj] / kc[ki] * (1 - COST)})
    return out


def won(x):
    return f"{round(x):,}만원"


def block(e, title):
    h = np.array([x["h"] for x in e]); s = np.array([x["s"] for x in e]); m = 0.5 * h + 0.5 * s
    print(f"\n[{title}] 물린 경우 {len(e):,}건 · 종목 {len({x['code'] for x in e})}개")
    for nm, x in (("그냥 버티기", h), ("절반 옮기기", m), ("전부 옮기기", s)):
        v = 700 * x
        print(f"  {nm}  3년 뒤 500만원 아래(넣은 돈의 절반도 못 지킴) {np.mean(v < 500) * 100:.1f}% ({int((v < 500).sum())}건)"
              f" · 가운데 {won(np.median(v))} · 열 번 중 가장 나쁜 한 번 {won(np.quantile(v, 0.1))}"
              f" · 1,000만원 이상으로 회복 {np.mean(v >= 1000) * 100:.1f}%")
    return h, s, m


ev = events()
print(f"기준일 {ASOF} · 대형주 {len(codes)}종목 · 30% 물린 날 700만원 → 3년 뒤 · KODEX 200 · 옮길 때 비용 {COST * 100:.1f}%")
h, s, m = block(ev, "전체 (물린 날 2007~2023)")
block([x for x in ev if x["t"] <= DEV_END], "2007-02 ~ 2015 에 물린 경우")
block([x for x in ev if x["t"] > DEV_END], "2016 이후에 물린 경우")

print("\n[쉬운 말로] 3년 뒤 넣은 돈의 절반도 못 지킨 경우")
for nm, x in (("그냥 버티기", h), ("절반 옮기기", m), ("전부 옮기기", s)):
    p = float(np.mean(700 * x < 500))
    print(f"  {nm} {p * 100:.1f}% → " + ("한 번도 없음" if p == 0 else f"{1 / p:.1f}번 중 한 번꼴"))
print(f"  전부 옮기기가 그냥 버티기보다 더 남은 경우 {np.mean(s > h) * 100:.1f}%")

print("\n[대표 사례] LG디스플레이 2018-08-01 매수 → 물린 날 → 3년 뒤")
for x in ev:
    if x["code"] == "034220" and x["buy"] == "2018-08-01":
        print(f"  물린 날 {x['t']} · 끝 {x['end']} · 그냥 버티기 {won(700 * x['h'])} · 절반 옮기기 {won(700 * (x['h'] + x['s']) / 2)}"
              f" · 전부 옮기기 {won(700 * x['s'])}")

hb = collections.Counter(NAME[x["code"]] for x in ev if 700 * x["h"] < 500)
mb = collections.Counter(NAME[x["code"]] for x in ev if 700 * (x["h"] + x["s"]) / 2 < 500)
print("\n[넣은 돈의 절반도 못 지킨 경우 — 종목별]")
print("  그냥 버티기: " + " · ".join(f"{k} {v}" for k, v in hb.most_common()))
print("  절반 옮기기: " + " · ".join(f"{k} {v}" for k, v in mb.most_common()))
print("  물린 해별 건수: " + " · ".join(f"{k} {v}" for k, v in sorted(collections.Counter(x["t"][:4] for x in ev).items())))

print("\n[더 깊게 물렸을 때] 그냥 버티기가 전부 옮기기보다 더 남은 비율 · 3년 뒤 가운데 배수(버티기 대 전부)")
for loss in (0.70, 0.60, 0.50):
    e = events(loss)
    hh = np.array([x["h"] for x in e]); ss = np.array([x["s"] for x in e])
    print(f"  {round((1 - loss) * 100)}% 물림 {len(e):,}건 · 버티기가 더 남음 {np.mean(hh > ss) * 100:.1f}% · 가운데 {np.median(hh):.2f}배 대 {np.median(ss):.2f}배")

e2 = [x for x in ev if x["code"] != "028670"]
h2 = np.array([x["h"] for x in e2]); s2 = np.array([x["s"] for x in e2])
print(f"\n[STX팬오션을 뺀 판] 물린 경우 {len(e2):,}건 · 넣은 돈의 절반도 못 지킴: 그냥 버티기 {np.mean(700 * h2 < 500) * 100:.1f}%"
      f" · 절반 옮기기 {np.mean(700 * (h2 + s2) / 2 < 500) * 100:.1f}% · 전부 옮기기 {np.mean(700 * s2 < 500) * 100:.1f}%")

anym = []
for code in codes:
    d, c = SER[code]
    seen = set()
    for i, di in enumerate(d):
        if di < "2007-02-01" or di[:7] in seen:
            continue
        seen.add(di[:7])
        if di in kmap and i + H < len(c) and kmap[di] + H < len(kc):
            anym.append(c[i + H] / c[i] > kc[kmap[di] + H] / kc[kmap[di]])
print(f"\n[물리지 않은 아무 달] 한 종목을 사서 3년 든 쪽이 KODEX 200 보다 더 남은 비율 {np.mean(anym) * 100:.1f}% ({len(anym):,}번)"
      f" · 물린 날 버티기 {np.mean(h > s) * 100:.1f}%")
print("\n끝.")
