코인 · 갈아타기 · 상장폐지 · 실측 재현 페이지
2019년 10월, 1,000만원으로 그때 거래가 가장 많던 코인 20개(비트코인 포함)를 똑같이 나눠 산 두 사람입니다. ① 그대로 둔 사람 — 끝까지 그대로 둔다(비율도 다시 맞추지 않는다) ② 갈아 담은 사람 — 매달 첫날 그달 직전 30일 거래대금 상위 20개로 다시 골라 똑같이 나눠 담는다 바이낸스 USDT 현물 일봉 550개(지금 거래중 373 · 거래가 끝난 177 — 그중 티커만 바뀐 21 · 진짜 상장폐지 156)를 2026-09-25까지 썼습니다. 상장폐지된 코인은 마지막 날 들고 있던 몫의 50%만 건졌다고 쳤습니다.
| 상황 | 할 일 |
|---|---|
| 큰 코인 여러 개를 나눠 샀다 | 거래가 몰리는 코인으로 갈아타지 말고 처음 산 것을 그대로 둔다 |
| 거래가 갑자기 몰린 코인이 보인다 | 새로 들어온 코인은 들어온 뒤 3개월 동안 63.2%가 떨어졌다(원래 있던 코인 52.4%) |
처음 20개: ADA BCHABC BNB BTC EOS ETC ETH ICX IOTA LINK LTC NEO NULS ONT QTUM TRX VET WAVES XLM XRP
| 시작 | 그대로 둔 사람 | 갈아 담은 사람 | 더 남은 쪽 |
|---|---|---|---|
| 2019-10-01 | 6,532만원 | 1,452만원 | 그대로 |
| 2020-01-01 | 12,590만원 | 1,693만원 | 그대로 |
| 2020-04-01 | 12,525만원 | 1,913만원 | 그대로 |
| 2020-07-01 | 8,408만원 | 1,278만원 | 그대로 |
| 2020-10-01 | 4,753만원 | 995만원 | 그대로 |
| 2021-01-01 | 4,076만원 | 780만원 | 그대로 |
| 2021-04-01 | 737만원 | 239만원 | 그대로 |
| 2021-07-01 | 978만원 | 318만원 | 그대로 |
| 2021-10-01 | 736만원 | 226만원 | 그대로 |
| 2022-01-01 | 753만원 | 209만원 | 그대로 |
| 2022-04-01 | 725만원 | 252만원 | 그대로 |
| 2022-07-01 | 1,803만원 | 800만원 | 그대로 |
| 2022-10-01 | 1,298만원 | 759만원 | 그대로 |
| 2023-01-01 | 2,296만원 | 1,132만원 | 그대로 |
| 2023-04-01 | 1,087만원 | 723만원 | 그대로 |
| 2023-07-01 | 1,723만원 | 978만원 | 그대로 |
처음 20개를 고정한 채 매달 비율만 똑같이 다시 맞춘 계좌는 갈아 담기보다 15/16 더 남았습니다 — 손해의 큰 몫은 새 코인으로 갈아 담기에서 났습니다. 그대로 둔 계좌는 비율을 다시 맞춘 계좌보다 13/16 더 남았습니다. 매달 새로 들어온 코인 356번 중 63.2%가 들어온 뒤 3개월 동안 떨어졌습니다(원래 있던 코인 52.4%). 갈아 담은 바구니에 들어왔다 나간 코인 158개 중 25개가 나중에 바이낸스에서 상장폐지됐습니다(ALPACA BAKE BLZ BOND BURGER DENT FLM FRONT HIFI HIGH ICX LINA LRC MULTI NULS OMG OM PERP REN SXP TON TRU UNFI WAVES XMR). 그대로 둔 20개 중에는 3개(ICX NULS WAVES).
| 마지막 날 몫 중 건진 비율 | 그대로 둔 사람 | 갈아 담은 사람 |
|---|---|---|
| 100% | 6,567만원 | 1,506만원 |
| 50% | 6,532만원 | 1,452만원 |
| 0% | 6,498만원 | 1,399만원 |
돌리는 법: zip 을 풀고 python run_coinbasket.py (몇 분 걸립니다)
코인 550개(지금 거래중 373 · 거래가 끝난 177 — 그중 티커만 바뀐 21 · 진짜 상장폐지 156) 계좌 2019-10-01 ~ 2026-09-25 (6.99년) · 1,000만원 · 처음 20개 = 직전 30일 거래대금 상위 20개 처음 20개: ADA BCHABC BNB BTC EOS ETC ETH ICX IOTA LINK LTC NEO NULS ONT QTUM TRX VET WAVES XLM XRP ■ 두 사람 그대로 둔 사람 6,532만원 (가장 많았을 때보다 -85.2%까지 줄어든 적) 매달 갈아 담은 사람 1,452만원 (-90.8%) ■ 시작한 달을 3개월씩 옮겨 보면 (그대로 / 갈아 담기) 2019-10-01 6,532만원 / 1,452만원 그대로 2020-01-01 12,590만원 / 1,693만원 그대로 2020-04-01 12,525만원 / 1,913만원 그대로 2020-07-01 8,408만원 / 1,278만원 그대로 2020-10-01 4,753만원 / 995만원 그대로 2021-01-01 4,076만원 / 780만원 그대로 2021-04-01 737만원 / 239만원 그대로 2021-07-01 978만원 / 318만원 그대로 2021-10-01 736만원 / 226만원 그대로 2022-01-01 753만원 / 209만원 그대로 2022-04-01 725만원 / 252만원 그대로 2022-07-01 1,803만원 / 800만원 그대로 2022-10-01 1,298만원 / 759만원 그대로 2023-01-01 2,296만원 / 1,132만원 그대로 2023-04-01 1,087만원 / 723만원 그대로 2023-07-01 1,723만원 / 978만원 그대로 16번 중 그대로 둔 쪽이 더 남은 것 16번 ■ 한 코인 덕인가 · 비트코인을 빼면 · 개수를 바꾸면 (시작 달마다 그대로 > 갈아 담기 횟수) BNB 빼고 — 16/16 · 2019-10 시작 그대로 4,293만원 / 갈아 담기 1,469만원 BNB·BTC·ETH 빼고 — 16/16 · 2019-10 시작 그대로 3,326만원 / 갈아 담기 1,175만원 비트코인·금 토큰 빼고(알트만) — 16/16 · 2019-10 시작 그대로 6,342만원 / 갈아 담기 1,634만원 상위 10개로 바꾸면 — 16/16 상위 30개로 바꾸면 — 16/16 그대로 둔 계좌의 코인별 끝 금액(상위 3): BNB 2,454만원 · TRX 1,164만원 · ETH 744만원 ■ 손해는 어디서 — 갈아 담기 vs 비율만 다시 맞추기 처음 20개를 고정하고 매달 비율만 맞춘 계좌 > 갈아 담기 — 15/16 그대로 둔 계좌 > 비율만 맞춘 계좌 — 13/16 ■ 매달 새로 들어온 코인은 새로 들어온 코인 356번 중 들어온 뒤 3개월 동안 떨어진 것 63.2% · 원래 있던 코인 52.4% 갈아 담은 바구니에 들어왔다 나간 코인 158개 중 나중에 진짜 상장폐지 25개: ALPACA BAKE BLZ BOND BURGER DENT FLM FRONT HIFI HIGH ICX LINA LRC MULTI NULS OMG OM PERP REN SXP TON TRU UNFI WAVES XMR 그대로 둔 20개 중 나중에 진짜 상장폐지 3개: ICX NULS WAVES ■ 상장폐지 코인을 몇 % 건졌다고 치느냐 (그대로 / 갈아 담기) 100% 건짐 — 6,567만원 / 1,506만원 50% 건짐 — 6,532만원 / 1,452만원 0% 건짐 — 6,498만원 / 1,399만원 ■ 참고 갈아 담기를 지금 거래소에 남은 코인만으로 계산하면 2,680만원 갈아 담기 + 평균 가격 규칙(50일>200일 그리고 종가>30일일 때만 보유) 3,128만원 (-52.4%) · 그대로 둔 계좌가 더 남은 시작 달 9/16 비트코인 하나만 같은 날 사서 그대로 10,146만원 끝.
# -*- 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("끝.")
알고랩 Strategy Lab — 전략을 검증하고, 검증한 것만 자동매매로 옮깁니다. 거래가 몰리는 코인을 보면 갈아타고 싶어지는 마음을, 정해 둔 규칙대로만 움직이는 프로그램이 대신 지킵니다. 알고랩 자동매매 살펴보기 · 전략 검증 전체 보기
과거 데이터를 돌려 본 결과이며 미래 수익을 보장하지 않습니다. 투자 판단과 책임은 본인에게 있습니다.