# -*- coding: utf-8 -*-
"""코인 여러 개 — 거래 많은 코인으로 갈아탈까, 처음 산 걸 그대로 둘까 (AlgoLab 재현 코드 · 표준 라이브러리만)

같은 폴더 data/ 에 얼려 둔 바이낸스 USDT 현물 일봉(2018-01-01 ~ 2026-09-25)을 읽는다.
  data/coins.csv          코인 550개 · 지금 거래중 373 / 거래가 끝난 177 (바이낸스 exchangeInfo status 기준)
  data/coins/<코인>.csv    date,close,volume — 상장폐지된 코인은 바이낸스 공개 벌크(data.binance.vision)에서 받았다
  · 스테이블코인·레버리지 토큰(UP/DOWN/BULL/BEAR)·래핑 코인은 처음부터 뺐다
  · 7일 넘게 비는 자리, 하루 +900% 넘게 뛰는 자리(티커 재사용·액면 교체)에서 그 코인의 기록을 잘랐다

두 계좌(1,000만원 · 그날까지 250일 넘게 거래된 코인이 처음 20개를 넘긴 달 = 2019-10-01 부터 2026-09-25 까지):
  · 처음 20개 = 시작일 직전 30일 거래대금(종가×거래량) 상위 20개(비트코인 포함) — 똑같이 나눠 산다
  · 그대로 둔 사람: 끝까지 그대로 둔다(비율도 다시 맞추지 않는다)
  · 갈아 담은 사람: 매달 첫날 그달 직전 30일 거래대금 상위 20개로 다시 골라 똑같이 나눠 담는다
  · 상장폐지된 코인은 마지막 날 들고 있던 몫의 50%만 건졌다고 친다(0%·100% 판도 같이 낸다)
  · 티커만 바뀐 코인(RENAME 목록)은 마지막 날 가격 그대로 현금으로 바꿨다고 친다
  · 비용은 처음 살 때 0.1% 만 — 매달 갈아 담는 수수료는 넣지 않았다(갈아 담은 쪽에 유리한 가정)
  python run_coinbasket.py      (전부 도는 데 몇 분 걸린다)
"""
import bisect
import math
import os

HERE = os.path.dirname(os.path.abspath(__file__))
DATA = os.path.join(HERE, "data")
SEED = 1000.0           # 만원
COST = 0.001
TOP = 20
MIN_BARS = 250
VOLWIN = 30
WARM = 200
RENAME = {"MATIC": "POL", "FTM": "S", "EOS": "A", "MKR": "SKY", "RNDR": "RENDER", "AGIX": "FET", "OCEAN": "FET",
          "LEND": "AAVE", "ERD": "EGLD", "NPXS": "PUNDIX", "BTT": "BTTC", "GXS": "REI", "TOMO": "VIC",
          "MFT": "HIFI", "KEEP": "T", "NU": "T", "BCHABC": "BCH", "XZC": "FIRO", "STRAT": "STRAX",
          "BZRX": "OOKI", "MC": "BEAMX", "GAL": "G", "EPS": "EPX", "PAX": "USDP",
          "BCC": "BCHABC", "BNX": "FORM", "KLAY": "KAIA"}


# ── 자료 ─────────────────────────────────────────────────────────────────
meta = []
with open(os.path.join(DATA, "coins.csv"), encoding="utf-8") as f:
    next(f)
    for line in f:
        s, st, *_ = line.rstrip("\n").split(",")
        meta.append((s, st == "거래중"))
SYMS = [m[0] for m in meta]
LIVE = [m[1] for m in meta]
K = len(SYMS)
D, C, V = [], [], []
for s in SYMS:
    d, c, v = [], [], []
    with open(os.path.join(DATA, "coins", s + ".csv"), encoding="utf-8") as f:
        next(f)
        for line in f:
            a, b, x = line.rstrip("\n").split(",")
            d.append(a); c.append(float(b)); v.append(float(x))
    D.append(d); C.append(c); V.append(v)
ALLD = sorted({x for d in D for x in d})
IDX = {x: i for i, x in enumerate(ALLD)}
N = len(ALLD)
SL = [[IDX[x] for x in d] for d in D]
R0 = [sl[0] for sl in SL]
R1 = [sl[-1] for sl in SL]
DEAD = [not x for x in LIVE]
REN = [DEAD[j] and SYMS[j][:-4] in RENAME for j in range(K)]
REAL_DEAD = [DEAD[j] and not REN[j] for j in range(K)]
MS = [0] + [i for i in range(1, N) if ALLD[i][:7] != ALLD[i - 1][:7]] + [N]
FIRST = next(i for i in range(N) if any(i - R0[j] >= MIN_BARS and i <= R1[j] for j in range(K)))
FIRST = max(FIRST, WARM)

# 달력 위 거래대금 누적합(오늘 제외 30일 창용)
QVC = []
for j in range(K):
    row = [0.0] * (N + 1)
    qv = dict(zip(SL[j], (c * v for c, v in zip(C[j], V[j]))))
    acc = 0.0
    for i in range(N):
        row[i] = acc
        acc += qv.get(i, 0.0)
    row[N] = acc
    QVC.append(row)


def sma(x, n):
    out = [math.nan] * len(x)
    s = 0.0
    for i, v in enumerate(x):
        s += v
        if i >= n:
            s -= x[i - n]
        if i >= n - 1:
            out[i] = s / n
    return out


def growth(c, pos):
    """코인 자기 행 위의 하루 성장률 — 종가 판정·그 종가 체결 · 바뀐 비중만큼 0.1%."""
    g = [1.0] * len(c)
    prev = 0.0
    for i in range(len(c)):
        r = c[i] / c[i - 1] - 1 if i else 0.0
        g[i] = (1 + pos[i] * r) * (1 - COST * abs(pos[i] - prev))
        prev = pos[i]
    return g


def bot_pos(c):
    f, s, m = sma(c, 50), sma(c, 200), sma(c, 30)
    sig = [1.0 if (not math.isnan(f[i]) and not math.isnan(s[i]) and f[i] > s[i]
                   and not math.isnan(m[i]) and c[i] > m[i]) else 0.0 for i in range(len(c))]
    return [0.0] + sig[:-1]            # 오늘 종가로 판정 → 그 종가부터 들고 있는 몫


def calendar(pos_of):
    """코인별 (달력 성장률 dict, 달력 비중 dict, 마지막 날 비중)."""
    out = []
    for j in range(K):
        pos = pos_of(j)
        g = growth(C[j], pos)
        out.append((dict(zip(SL[j], g)), dict(zip(SL[j], pos)), pos[-1]))
    return out


HOLD = calendar(lambda j: [0.0] + [1.0] * (len(C[j]) - 1))
BOT = calendar(lambda j: bot_pos(C[j]))


def basket(M, pool, first, top=20, recov=0.5, mode="swap", track=False):
    """mode: swap = 매달 다시 골라 똑같이 담기 · fixed = 첫 달 고른 것 그대로 · rebal = 첫 달 고른 것을 매달 똑같이 다시 맞추기.
    first 는 달 첫날 인덱스 — 그 전날 종가에 사서 first 날부터 센다."""
    eq = [SEED] * N
    E = SEED
    members = {}
    chosen = None
    W = None
    for a, e in zip(MS[:-1], MS[1:]):
        if a < first:
            continue
        if mode == "swap" or chosen is None:
            mem = [j for j in range(K) if pool[j] and a - R0[j] >= MIN_BARS and a <= R1[j]]
            if len(mem) > top:
                lo = max(a - VOLWIN, 0)
                mem.sort(key=lambda j: -(QVC[j][a] - QVC[j][lo]))   # 같은 값이면 이름 순(안정 정렬)
                mem = sorted(mem[:top])
            chosen = mem
        if mode == "rebal":
            mem = [j for j in chosen if a <= R1[j]]                 # 폐지된 몫은 현금으로 남아 나머지에 다시 나뉜다
        else:
            mem = chosen
        if track:
            members[ALLD[a]] = [SYMS[j] for j in mem]
        if mode != "fixed" or W is None:
            W = {j: E / len(mem) for j in mem}
        for i in range(a, e):
            tot = 0.0
            for j in mem:
                gd, pd, last = M[j]
                g = gd.get(i, 1.0) if i <= R1[j] else 1.0
                if DEAD[j] and i == R1[j]:
                    g *= 1 - last * (1 - (1.0 if REN[j] else recov))
                W[j] *= g
                tot += W[j]
            eq[i] = tot
        E = eq[e - 1]
    peak = SEED
    mdd = 0.0
    for i in range(first, N):
        peak = max(peak, eq[i])
        mdd = min(mdd, eq[i] / peak - 1)
    return dict(만원=eq[N - 1], 낙폭=mdd, members=members)


def relevant(first):
    rel = [False] * K
    for a, e in zip(MS[:-1], MS[1:]):
        if e <= first:
            continue
        for j in range(K):
            if a - R0[j] >= MIN_BARS and a <= R1[j]:
                rel[j] = True
    return rel


def won(x):
    return f"{x:,.0f}만원"


FIRST20 = next(a for a in MS[:-1] if a >= FIRST and sum(1 for j in range(K) if a - R0[j] >= MIN_BARS and a <= R1[j]) >= 20)
STARTS = [a for a in MS[:-1] if a >= FIRST20 and ALLD[a][5:7] in ("01", "04", "07", "10") and a <= MS[-1] - 3 * 365]

if __name__ == "__main__":
    first = FIRST20
    yrs = (N - first) / 365.25
    ALL = [True] * K
    si = {s: j for j, s in enumerate(SYMS)}
    print(f"코인 {K}개(지금 거래중 {sum(LIVE)} · 거래가 끝난 {sum(DEAD)} — 그중 티커만 바뀐 {sum(REN)} · 진짜 상장폐지 {sum(REAL_DEAD)})")
    print(f"계좌 {ALLD[first]} ~ {ALLD[-1]} ({yrs:.2f}년) · 1,000만원 · 처음 20개 = 직전 30일 거래대금 상위 20개")
    A = basket(HOLD, ALL, first, mode="fixed", track=True)
    Cw = basket(HOLD, ALL, first, mode="swap", track=True)
    print("  처음 20개:", " ".join(s[:-4] for s in A["members"][ALLD[first]]))
    print("\n■ 두 사람")
    print(f"  그대로 둔 사람      {won(A['만원'])}  (가장 많았을 때보다 {A['낙폭']*100:.1f}%까지 줄어든 적)")
    print(f"  매달 갈아 담은 사람  {won(Cw['만원'])}  ({Cw['낙폭']*100:.1f}%)")

    print("\n■ 시작한 달을 3개월씩 옮겨 보면 (그대로 / 갈아 담기)")
    win = 0
    for a in STARTS:
        x = basket(HOLD, ALL, a, mode="fixed")["만원"]
        y = basket(HOLD, ALL, a, mode="swap")["만원"]
        win += x > y
        print(f"  {ALLD[a]}  {won(x):>10} / {won(y):>10}  {'그대로' if x > y else '갈아 담기'}")
    print(f"  {len(STARTS)}번 중 그대로 둔 쪽이 더 남은 것 {win}번")

    print("\n■ 한 코인 덕인가 · 비트코인을 빼면 · 개수를 바꾸면 (시작 달마다 그대로 > 갈아 담기 횟수)")
    for name, drop in (("BNB 빼고", ("BNBUSDT",)), ("BNB·BTC·ETH 빼고", ("BNBUSDT", "BTCUSDT", "ETHUSDT")),
                       ("비트코인·금 토큰 빼고(알트만)", ("BTCUSDT", "PAXGUSDT"))):
        pool = [s not in drop for s in SYMS]
        w = sum(basket(HOLD, pool, a, mode="fixed")["만원"] > basket(HOLD, pool, a, mode="swap")["만원"] for a in STARTS)
        f0 = basket(HOLD, pool, first, mode="fixed")["만원"]
        s0 = basket(HOLD, pool, first, mode="swap")["만원"]
        print(f"  {name} — {w}/{len(STARTS)} · 2019-10 시작 그대로 {won(f0)} / 갈아 담기 {won(s0)}")
    for n in (10, 30):
        w = sum(basket(HOLD, ALL, a, top=n, mode="fixed")["만원"] > basket(HOLD, ALL, a, top=n, mode="swap")["만원"] for a in STARTS)
        print(f"  상위 {n}개로 바꾸면 — {w}/{len(STARTS)}")
    m0 = [si[s] for s in A["members"][ALLD[first]]]
    fin = []
    for j in m0:
        one = basket(HOLD, [k == j for k in range(K)], first, top=1, mode="fixed")["만원"]
        fin.append((one / len(m0), SYMS[j][:-4]))
    fin.sort(reverse=True)
    print("  그대로 둔 계좌의 코인별 끝 금액(상위 3):", " · ".join(f"{n} {won(v)}" for v, n in fin[:3]))

    print("\n■ 손해는 어디서 — 갈아 담기 vs 비율만 다시 맞추기")
    rb = sum(basket(HOLD, ALL, a, mode="rebal")["만원"] > basket(HOLD, ALL, a, mode="swap")["만원"] for a in STARTS)
    fr = sum(basket(HOLD, ALL, a, mode="fixed")["만원"] > basket(HOLD, ALL, a, mode="rebal")["만원"] for a in STARTS)
    print(f"  처음 20개를 고정하고 매달 비율만 맞춘 계좌 > 갈아 담기 — {rb}/{len(STARTS)}")
    print(f"  그대로 둔 계좌 > 비율만 맞춘 계좌 — {fr}/{len(STARTS)}")

    print("\n■ 매달 새로 들어온 코인은")
    keys = sorted(Cw["members"])
    ms_e = {ALLD[a]: e for a, e in zip(MS[:-1], MS[1:])}
    new3, old3 = [], []
    prev = None
    for k_i, d in enumerate(keys):
        a = IDX[d]
        e3 = ms_e[keys[min(k_i + 2, len(keys) - 1)]]
        for s in Cw["members"][d]:
            j = si[s]
            g = 1.0
            for i in range(a, e3):
                g *= HOLD[j][0].get(i, 1.0) if i <= R1[j] else 1.0
            if prev is not None:
                (new3 if s not in prev else old3).append(g - 1)
        prev = set(Cw["members"][d])
    print(f"  새로 들어온 코인 {len(new3)}번 중 들어온 뒤 3개월 동안 떨어진 것 {sum(x < 0 for x in new3) / len(new3) * 100:.1f}%"
          f" · 원래 있던 코인 {sum(x < 0 for x in old3) / len(old3) * 100:.1f}%")
    ever = sorted({s for v in Cw["members"].values() for s in v})
    dead_in = [s for s in ever if REAL_DEAD[si[s]]]
    fixed_dead = [s for s in A["members"][ALLD[first]] if REAL_DEAD[si[s]]]
    print(f"  갈아 담은 바구니에 들어왔다 나간 코인 {len(ever)}개 중 나중에 진짜 상장폐지 {len(dead_in)}개: {' '.join(s[:-4] for s in dead_in)}")
    print(f"  그대로 둔 20개 중 나중에 진짜 상장폐지 {len(fixed_dead)}개: {' '.join(s[:-4] for s in fixed_dead)}")

    print("\n■ 상장폐지 코인을 몇 % 건졌다고 치느냐 (그대로 / 갈아 담기)")
    for rv in (1.0, 0.5, 0.0):
        print(f"  {int(rv*100)}% 건짐 — {won(basket(HOLD, ALL, first, recov=rv, mode='fixed')['만원'])} / "
              f"{won(basket(HOLD, ALL, first, recov=rv, mode='swap')['만원'])}")

    print("\n■ 참고")
    rel = relevant(first)
    live_only = [not (rel[j] and DEAD[j]) for j in range(K)]
    print(f"  갈아 담기를 지금 거래소에 남은 코인만으로 계산하면 {won(basket(HOLD, live_only, first, mode='swap')['만원'])}")
    bs = basket(BOT, ALL, first, mode="swap")
    rw = sum(basket(HOLD, ALL, a, mode="fixed")["만원"] > basket(BOT, ALL, a, mode="swap")["만원"] for a in STARTS)
    print(f"  갈아 담기 + 평균 가격 규칙(50일>200일 그리고 종가>30일일 때만 보유) {won(bs['만원'])} ({bs['낙폭']*100:.1f}%)"
          f" · 그대로 둔 계좌가 더 남은 시작 달 {rw}/{len(STARTS)}")
    j = si["BTCUSDT"]
    print(f"  비트코인 하나만 같은 날 사서 그대로 {won(basket(HOLD, [k == j for k in range(K)], first, top=1, mode='fixed')['만원'])}")
    print()
    print("끝.")
