비트코인 봇 · 자금 관리 · 주문 크기 · 실측 재현 페이지
봇에 돈을 전부 넣으면 떨어질 때 계좌도 그만큼 줄어듭니다. 그래서 봇이 한 번 살 때 쓰는 돈을 내가 참을 수 있는 손실에 맞춰 정하는 공식을 실제 가격으로 쟀습니다. 업비트 원화와 바이낸스 USDT 4시간봉을 각각 돌려 나쁜 쪽 숫자를 씁니다.
| 항목 | 내용 |
|---|---|
| 공식 | 봇 주문 크기(계좌 전체의 %) = 참을 손실 × 0.8 ÷ 봇의 과거 최악 손실 — 나머지는 현금 |
| 참을 손실 | 계좌가 가장 많았을 때보다 몇 % 줄어드는 것까지 참을지. 계좌가 1,000만원일 때 800만원까지 줄어도 봇을 그대로 둘 수 있으면 20% |
| 봇의 과거 최악 손실 | 그 봇에 돈을 전부 넣었을 때 계좌가 가장 크게 줄었던 폭(백테스트의 최대 낙폭). 거래소마다 다르면 큰 쪽 — 이 봇은 2017년 12월~2023년 12월 업비트 31.9% · 바이낸스 47.7%(2018년 하락장) → 48% |
| 계산 | 20 × 0.8 ÷ 48 = 33.3 → 주문 크기 33%(내림). 참을 손실 10%면 16% · 30%면 50% |
| 거는 법 | 봇 설정의 주문 크기를 「계좌 전체의 33%」로 겁니다(트레이딩뷰 전략이면 주문 크기 = 자기자본의 33%). 봇 몫을 처음에 한 번만 떼어 두고 그대로 두면 33번 중 6번 참을 선을 넘었습니다 — 봇이 크게 번 뒤 봇 몫이 커지기 때문입니다 |
| 0.8 | 앞으로는 과거보다 더 크게 잃을 수 있어서 두는 여유입니다. 이 비트코인 봇에선 0.8 없이(41%)도 넘은 적이 없었지만(같은 기간 13.4%), 이더리움 봇에선 0.8 을 곱해도 넘은 때가 있었습니다 |
| 계좌 | 가장 크게 줄어든 폭 | 끝 금액 |
|---|---|---|
| 봇에 전부 | 28.0% | 1,540만원 |
| 공식대로(주문 크기 33%) | 11.1% | 1,191만원 |
| 공식 비중을 처음에 떼어 두고 그대로 | 11.7% | 1,178만원 |
| 비트코인 그냥 보유 | 53.4% | 1,968만원 |
두 거래소 중 나쁜 쪽(끝 금액은 작은 쪽 · 줄어든 폭은 큰 쪽). 공식은 수익을 늘리지 않습니다 — 번 돈은 봇에 전부 넣은 계좌보다, 그냥 보유보다 적었습니다. 공식이 지켜 주는 것은 정해 둔 손실 안에서 봇을 계속 돌리는 것입니다.
맞습니다 — 공식은 넣는 돈을 줄여서 손실을 줄입니다. 그래서 봇 없이 비트코인을 같은 공식으로 사는 계좌도 같이 쟀습니다(비트코인 보유의 과거 최악 손실 87.0% → 비중 18%). 같은 33번에서 참을 선을 넘은 경우: 다시 맞추는 주기에 따라 5~15번(안 맞춤 15번 · 매달 6번 · 분기마다 5번 · 해마다 14번 · 매 봉 7번). 봇 공식은 0번이었습니다. 봇은 흐름이 꺾이면 팔고 현금으로 나와 있기 때문입니다.
돌리는 법: zip 을 풀고 pip install numpy 뒤 python run_botsize.py (몇 분 걸립니다).
[자료] 비트코인 업비트: 19,824봉 · 2017-09-26T08:00 ~ 2026-10-13T16:00 · 4시간 넘게 빈 자리 3곳(가장 긴 8시간)
[자료] 비트코인 바이낸스: 20,052봉 · 2017-08-17T04:00 ~ 2026-10-13T16:00 · 4시간 넘게 빈 자리 8곳(가장 긴 32시간)
[자료] 이더리움 업비트: 19,801봉 · 2017-09-26T08:00 ~ 2026-10-13T16:00 · 4시간 넘게 빈 자리 5곳(가장 긴 84시간)
[자료] 이더리움 바이낸스: 20,052봉 · 2017-08-17T04:00 ~ 2026-10-13T16:00 · 4시간 넘게 빈 자리 8곳(가장 긴 32시간)
[본편 · 비트코인 봇 · 1,000만원 · 참을 손실 20% · 공식 정한 날 2024-01-01 · 두 거래소 공통 시작 2017-09-26]
봇의 과거 최악 손실(2017-12 ~ 2023-12): 업비트 31.9% · 바이낸스 47.7% → 큰 쪽 47.7% (정수로 48%)
주문 크기 = 20 × 0.8 ÷ 48 → 계좌 전체의 33%
2018년 하락장 뒤(2019-03 ~)로 봇에 전부 넣은 계좌가 가장 크게 줄어든 폭: 업비트 31.9%(2021-11-09 → 2022-10-24) · 바이낸스 35.7%(2021-11-09 → 2022-10-14)
봇에 전부: 가장 크게 줄어든 폭 28.0% · 끝 1,540만원
공식대로: 가장 크게 줄어든 폭 11.1% · 끝 1,191만원
비트코인 그냥 보유: 가장 크게 줄어든 폭 53.4% · 끝 1,968만원
공식 비중을 처음에 떼어 두고 그대로: 가장 크게 줄어든 폭 11.7% · 끝 1,178만원
공식 비중을 매 봉 다시 맞춤(연구 판): 가장 크게 줄어든 폭 9.9% · 끝 1,173만원
[업비트 2024-01-01~2026-10-13T16:00] 봇이 들고 있던 시간 36% · 사고판 횟수 146번
봇에 전부: 고점 1,602만원(2024-03-05) → 바닥 1,177만원(2024-10-13) · 줄어든 폭 26.5% · 끝 2,144만원
공식대로: 고점 1,203만원(2024-03-05) → 바닥 1,078만원(2024-10-13) · 줄어든 폭 10.4% · 끝 1,335만원
그냥 보유: 고점 3,130만원(2025-10-09) → 바닥 1,555만원(2026-08-14) · 줄어든 폭 50.3% · 끝 1,971만원
[바이낸스 2024-01-01~2026-10-13T16:00] 봇이 들고 있던 시간 37% · 사고판 횟수 156번
봇에 전부: 고점 1,356만원(2024-03-04) → 바닥 977만원(2024-09-18) · 줄어든 폭 28.0% · 끝 1,540만원
공식대로: 고점 1,134만원(2024-03-04) → 바닥 1,009만원(2024-09-18) · 줄어든 폭 11.1% · 끝 1,191만원
그냥 보유: 고점 2,963만원(2025-10-06) → 바닥 1,379만원(2026-06-30) · 줄어든 폭 53.4% · 끝 1,968만원
공식대로 한 계좌가 참을 선(20%) 안: 예
[참을 손실을 바꾸면 · 같은 기간]
참을 손실 10% → 주문 크기 16% · 가장 크게 줄어든 폭 5.7% · 끝 1,093만원
참을 손실 20% → 주문 크기 33% · 가장 크게 줄어든 폭 11.1% · 끝 1,191만원
참을 손실 30% → 주문 크기 50% · 가장 크게 줄어든 폭 15.9% · 끝 1,286만원
(0.8 없이 · 계수 1.0) 주문 크기 41% · 가장 크게 줄어든 폭 13.4%
[다른 때 시작해도 · 비트코인 봇 · 2019-07 ~ 2024-07 반년마다 시작한 2년 계좌 11개 × 참을 손실 10·20·30%]
계수 0.8: 33번 중 참을 선을 넘은 경우 — 공식대로(주문 크기) 0번 · 처음에 떼어 두고 그대로 6번 · 매 봉 맞춤 0번 · 봇에 전부 27번
계수 1.0: 33번 중 참을 선을 넘은 경우 — 공식대로(주문 크기) 0번 · 처음에 떼어 두고 그대로 9번 · 매 봉 맞춤 0번 · 봇에 전부 27번
[다른 때 시작해도 · 이더리움 봇 · 2019-07 ~ 2024-07 반년마다 시작한 2년 계좌 11개 × 참을 손실 10·20·30%]
계수 0.8: 33번 중 참을 선을 넘은 경우 — 공식대로(주문 크기) 6번 · 처음에 떼어 두고 그대로 12번 · 매 봉 맞춤 0번 · 봇에 전부 33번
계수 1.0: 33번 중 참을 선을 넘은 경우 — 공식대로(주문 크기) 11번 · 처음에 떼어 두고 그대로 12번 · 매 봉 맞춤 0번 · 봇에 전부 33번
공식대로 한 계좌가 넘은 경우: 2019-07-01 시작·참을 10%·13.9% · 2019-07-01 시작·참을 20%·22.5% · 2020-01-01 시작·참을 10%·13.9% · 2020-01-01 시작·참을 20%·22.5% · 2020-07-01 시작·참을 10%·13.9% · 2020-07-01 시작·참을 20%·22.5%
[반론 · 봇 없이 비트코인을 같은 공식으로 사면(보유의 과거 최악 손실로 비중) · 같은 33번]
다시 맞추는 주기 안 맞춤: 참을 선을 넘은 경우 15번 · 끝 금액이 봇 공식 쪽이 더 많은 경우 19번
다시 맞추는 주기 매달: 참을 선을 넘은 경우 6번 · 끝 금액이 봇 공식 쪽이 더 많은 경우 24번
다시 맞추는 주기 분기마다: 참을 선을 넘은 경우 5번 · 끝 금액이 봇 공식 쪽이 더 많은 경우 21번
다시 맞추는 주기 해마다: 참을 선을 넘은 경우 14번 · 끝 금액이 봇 공식 쪽이 더 많은 경우 19번
다시 맞추는 주기 매 봉: 참을 선을 넘은 경우 7번 · 끝 금액이 봇 공식 쪽이 더 많은 경우 27번
봇 공식(주문 크기)이 넘은 경우: 0번
비트코인 보유의 과거 최악 손실 87.0% → 같은 공식 비중 18%
[한계 · 이더리움 봇(이동평균 20/200 · 일봉 50일선) · 2024-01 ~ · 참을 손실 20%] 과거 최악 손실 68.6% → 주문 크기 23%
봇에 전부: 가장 크게 줄어든 폭 36.2% · 끝 2,095만원
공식대로: 가장 크게 줄어든 폭 10.9% · 끝 1,252만원
이더리움 그냥 보유: 가장 크게 줄어든 폭 68.0% · 끝 1,098만원
끝.
# -*- coding: utf-8 -*-
"""AlgoLab · 비트코인 봇에 얼마 넣을까 — 재현 코드 (https://algolab.co.kr/botsize)
공식: 봇 주문 크기(계좌 전체의 %) = 참을 손실 × 0.8 ÷ 봇의 과거 최악 손실
· 참을 손실: 계좌가 가장 많았을 때보다 몇 % 줄어드는 것까지 참을지
· 봇의 과거 최악 손실: 그 봇에 돈을 전부 넣었을 때 계좌가 가장 크게 줄었던 폭(두 거래소 중 큰 값)
· 손으로 계산하는 대로 — 최악 손실은 정수 %로 반올림, 주문 크기는 정수 %로 내림
봇(예시): 비트코인 4시간봉 · 슈퍼트렌드(ATR 10 · 배수 3)가 상승이면 산다 · 단 일봉 50일 이동평균(4시간봉 300개) 위에서만.
신호가 나온 봉의 종가에 사고판다(다음 봉부터 보유) · 사고팔 때마다 0.1% 비용.
계좌(처방 그대로): 봇이 살 때마다 그 순간 계좌 전체의 x 만 사고, 팔 때 전부 판다. 나머지는 현금(이자 0).
비교용으로 「봇 몫을 처음에 떼어 두고 그대로」 · 「매 봉 비중을 다시 맞춤」(연구 판) 계좌도 같이 낸다.
업비트 원화·바이낸스 USDT 를 각각 돌려 나쁜 쪽(끝 금액은 작은 쪽 · 줄어든 폭은 큰 쪽)을 쓴다.
자료: data_botsize/ (2026-10-13 16:00 UTC 시작 4시간봉까지 얼림) · numpy 만 쓴다.
python run_botsize.py
"""
import csv, json, os, sys
import numpy as np
HERE = os.path.dirname(os.path.abspath(__file__))
DATA = os.path.join(HERE, "data_botsize")
COST = 0.001
WARM = 400
BPD = 6 # 4시간봉 하루 6개
SEED = 10_000_000
K = 0.8
HOLD_START = "2024-01-01" # 공식을 정한 날(이 날 전까지의 이력만 본다)
ASOF = "2026-10-13T16:00:00"
BOTS = {
"비트코인": dict(up="KRW-BTC", bi="BTCUSDT", strat="supertrend", prm=dict(n=10, mult=3.0), filt=50),
"이더리움": dict(up="KRW-ETH", bi="ETHUSDT", strat="ma_cross", prm=dict(fast=20, slow=200), filt=50),
}
# ── 지표 ─────────────────────────────────────────────────────────────
def sma(x, n):
out = np.full(len(x), np.nan)
if len(x) >= n:
cs = np.cumsum(np.insert(x, 0, 0.0))
out[n - 1:] = (cs[n:] - cs[:-n]) / n
return out
def atr(h, l, c, n):
pc = np.r_[c[0], c[:-1]]
tr = np.maximum(h - l, np.maximum(np.abs(h - pc), np.abs(l - pc)))
a = np.full(len(tr), np.nan)
if len(tr) >= n:
a[n - 1] = tr[:n].mean()
for i in range(n, len(tr)):
a[i] = a[i - 1] + (tr[i] - a[i - 1]) / n
return a
def supertrend(o, h, l, c, n=10, mult=3.0):
a = atr(h, l, c, n); hl2 = (h + l) / 2
up = hl2 - mult * a; dn = hl2 + mult * a
d = np.ones(len(c))
for i in range(1, len(c)):
if np.isnan(a[i]):
continue
if c[i - 1] > up[i - 1]:
up[i] = max(up[i], up[i - 1])
if c[i - 1] < dn[i - 1]:
dn[i] = min(dn[i], dn[i - 1])
if c[i] > dn[i - 1]:
d[i] = 1
elif c[i] < up[i - 1]:
d[i] = -1
else:
d[i] = d[i - 1]
return (d > 0).astype(float)
def ma_cross(o, h, l, c, fast=20, slow=200):
f, s = sma(c, fast), sma(c, slow)
out = np.zeros(len(c)); m = ~np.isnan(f) & ~np.isnan(s)
out[m] = (f[m] > s[m]).astype(float)
return out
STRATS = {"supertrend": supertrend, "ma_cross": ma_cross}
# ── 자료 ─────────────────────────────────────────────────────────────
def norm(d):
return d if "T" in d else d + "T00:00:00"
def load(path, start=None, end=ASOF):
d, cols = [], [[], [], [], []]
with open(path, encoding="utf-8") as f:
next(f)
for p in csv.reader(f):
dd = norm(p[0])
if (start and dd < start) or dd > end:
continue
d.append(dd)
for i in range(4):
cols[i].append(float(p[i + 1]))
o, h, l, c = (np.asarray(x) for x in cols)
ret = np.zeros(len(c)); ret[1:] = c[1:] / c[:-1] - 1
return dict(d=np.asarray(d), o=o, h=h, l=l, c=c, ret=ret, n=len(c))
def gaps(S, bars=1):
"""4시간 넘게 빈 자리 수와 가장 긴 빈 자리(시간)."""
import datetime as dt
t = [dt.datetime.fromisoformat(x) for x in S["d"]]
hrs = np.array([(b - a).total_seconds() / 3600 for a, b in zip(t[:-1], t[1:])])
return int((hrs > 4 * bars).sum()), float(hrs.max()) if len(hrs) else 0.0
def bot_pos(S, cfg):
sig = STRATS[cfg["strat"]](S["o"], S["h"], S["l"], S["c"], **cfg["prm"])
sig = np.clip(np.nan_to_num(sig), 0.0, 1.0)
if cfg["filt"]:
m = sma(S["c"], cfg["filt"] * BPD)
f = np.zeros(S["n"]); ok = ~np.isnan(m)
f[ok] = (S["c"][ok] > m[ok]).astype(float)
sig = sig * f
return np.r_[0.0, sig[:-1]] # 신호 봉 종가에 체결 → 다음 봉부터 보유
def bot_growth(S, pos):
"""봇 계좌(자기 돈 전부 사고 전부 팔기) 봉별 성장률."""
turn = np.abs(np.diff(np.r_[0.0, pos]))
return (1.0 + pos * S["ret"]) * (1.0 - COST * turn)
def rebal_growth(S, w):
"""연구 풀 판 — 매 봉 목표 비중 w 로 다시 맞춘다(드리프트 회전 과금)."""
ret = S["ret"]
wprev = np.r_[0.0, w[:-1]]; rprev = np.r_[0.0, ret[:-1]]
den = 1.0 + wprev * rprev
wd = np.where(den > 1e-12, wprev * (1.0 + rprev) / den, wprev)
return (1.0 + w * ret) * (1.0 - COST * np.abs(w - wd))
def mdd(eq):
peak = np.maximum.accumulate(eq)
return float((eq / peak - 1.0).min())
def path_stats(eq, d):
peak = np.maximum.accumulate(eq)
dd = eq / peak - 1.0
k = int(np.argmin(dd)); j = int(np.argmax(eq[:k + 1]))
return dict(끝=round(float(eq[-1])), 최대낙폭=round(float(dd.min()), 4),
고점금액=round(float(eq[j])), 고점날=d[j][:10], 바닥금액=round(float(eq[k])), 바닥날=d[k][:10],
최저금액=round(float(eq.min())), 최저날=d[int(np.argmin(eq))][:10])
def accounts(S, pos, a, b, x):
"""봉 a-1 종가에 원금 SEED 로 시작해 봉 b-1 종가까지 — 계좌 넷의 금액 경로(첫 값 = 시작 시점).
시작할 때 봇 신호가 이미 켜져 있으면(pos[a]=1) 그 종가에 사며 편도 비용을 낸다.
"""
gb = bot_growth(S, pos)[a:b].copy()
gb[0] = (1.0 + pos[a] * S["ret"][a]) * (1.0 - COST * pos[a])
bot = SEED * np.r_[1.0, np.cumprod(gb)]
hold = SEED * (1 - COST) * np.r_[1.0, S["c"][a:b] / S["c"][a - 1]]
hold[0] = SEED
rule = x * bot + (1 - x) * SEED # 봇 계좌 + 통장(따로)
gr = rebal_growth(S, x * pos)[a:b].copy()
gr[0] = (1.0 + x * pos[a] * S["ret"][a]) * (1.0 - COST * x * pos[a])
rule_rb = SEED * np.r_[1.0, np.cumprod(gr)]
return dict(규칙=entry_sized(S, pos, a, b, x), 전부봇=bot, 존버=hold, 규칙_따로둠=rule, 규칙_매봉맞춤=rule_rb)
def entry_sized(S, pos, a, b, x):
"""★ 처방 그대로 — 봇이 살 때마다 '그 순간 계좌 전체의 x' 만큼만 사고, 팔 때 전부 판다.
산 뒤에는 코인 값이 움직이는 대로 둔다(중간에 다시 맞추지 않는다). 나머지는 현금(이자 0).
"""
c = S["c"]
cash, coin = float(SEED), 0.0
eq = np.empty(b - a + 1); eq[0] = SEED
for t in range(a, b):
prev = pos[t - 1] if t > a else 0.0
if pos[t] > 0 and prev == 0: # 봉 t-1 종가에 산다
tot = cash + coin * c[t - 1]
buy = x * tot
coin = buy * (1 - COST) / c[t - 1]
cash = tot - buy
elif pos[t] == 0 and prev > 0: # 봉 t-1 종가에 판다
cash += coin * c[t - 1] * (1 - COST)
coin = 0.0
eq[t - a + 1] = cash + coin * c[t]
return eq
def hold_rule(S, a, b, xh, rebalance_bars=0):
"""그냥 비트코인을 같은 공식으로 — 계좌의 xh 만 사서 들고 나머지 현금. rebalance_bars>0 이면 그 간격마다 xh 로 다시 맞춘다."""
c = S["c"]
coin = SEED * xh * (1 - COST) / c[a - 1]; cash = SEED * (1 - xh)
eq = np.empty(b - a + 1); eq[0] = SEED
for t in range(a, b):
if rebalance_bars and t > a and (t - a) % rebalance_bars == 0:
tot = cash + coin * c[t - 1]
tgt = xh * tot; cur = coin * c[t - 1]
cash -= (tgt - cur) + COST * abs(tgt - cur)
coin = tgt / c[t - 1]
eq[t - a + 1] = cash + coin * c[t]
return eq
def worst_M_hold(per, base):
out = {}
for mk, (S, pos) in per.items():
i0 = int(np.searchsorted(S["d"], base))
out[mk] = -mdd(S["c"][WARM:i0] / S["c"][WARM])
return max(out.values()), out
def view_x(k, T, M):
"""시청자가 손으로 하는 계산 그대로 — 최악 손실은 정수 퍼센트로 반올림, 비중은 정수 퍼센트로 내림."""
Mp = int(round(M * 100))
return min(1.0, int(k * T * 100 / Mp * 100 + 1e-9) / 100.0)
def worst_M(per, base):
"""base 직전까지 이력에서 봇 100% 최대 낙폭(두 거래소 중 큰 값)."""
out = {}
for mk, (S, pos) in per.items():
i0 = int(np.searchsorted(S["d"], base))
g = bot_growth(S, pos)[WARM:i0]
out[mk] = -mdd(np.cumprod(np.r_[1.0, g]))
return max(out.values()), out
def series(cfg, start=None):
per = {}
for mk, sym in (("업비트", "upbit_" + cfg["up"]), ("바이낸스", "binance_" + cfg["bi"])):
per[mk] = load(os.path.join(DATA, sym + "_4h.csv"), start=start)
st = max(per["업비트"]["d"][0], per["바이낸스"]["d"][0]) # 두 거래소 공통 시작
for mk in per:
cfgf = os.path.join(DATA, ("upbit_" + cfg["up"]) if mk == "업비트" else ("binance_" + cfg["bi"]))
per[mk] = load(cfgf + "_4h.csv", start=st)
return {mk: (S, bot_pos(S, cfg)) for mk, S in per.items()}, st
def holdout(coin, T=0.20, base=HOLD_START, k=K, end=None):
cfg = BOTS[coin]
per, st = series(cfg)
M, Mm = worst_M(per, base)
x = view_x(k, T, M)
res = {}
for mk, (S, pos) in per.items():
a = int(np.searchsorted(S["d"], base)); b = S["n"] if end is None else int(np.searchsorted(S["d"], end))
acc = accounts(S, pos, a, b, x)
res[mk] = {lab: path_stats(eq, S["d"][a - 1:b]) for lab, eq in acc.items()}
res[mk]["봇보유비율"] = round(float(pos[a:b].mean()), 4)
res[mk]["매매횟수"] = int(np.abs(np.diff(np.r_[pos[a], pos[a:b]])).sum())
res[mk]["구간"] = f"{S['d'][a][:10]}~{S['d'][b-1][:16]}"
Mh, Mhm = worst_M_hold(per, base)
xh = view_x(k, T, Mh)
for mk, (S, pos) in per.items():
a = int(np.searchsorted(S["d"], base)); b = S["n"] if end is None else int(np.searchsorted(S["d"], end))
res[mk]["존버공식"] = path_stats(hold_rule(S, a, b, xh), S["d"][a - 1:b])
res[mk]["존버공식_해마다맞춤"] = path_stats(hold_rule(S, a, b, xh, rebalance_bars=6 * 365), S["d"][a - 1:b])
res["_존버공식"] = dict(M=round(Mh, 4), M_거래소별={m: round(v, 4) for m, v in Mhm.items()}, 비중=round(xh, 4))
labs = ["규칙", "전부봇", "존버", "규칙_따로둠", "규칙_매봉맞춤", "존버공식", "존버공식_해마다맞춤"]
mks = [m for m in res if not m.startswith("_")]
worse = {lab: dict(끝=min(res[m][lab]["끝"] for m in mks), 최대낙폭=min(res[m][lab]["최대낙폭"] for m in mks)) for lab in labs}
return dict(코인=coin, 공통시작=st[:10], 기준일=base, 견딜손실=T, 계수=k, M=round(M, 4), M_거래소별={m: round(v, 4) for m, v in Mm.items()},
봇비중=round(x, 4), 봇에=round(SEED * x), 통장에=round(SEED * (1 - x)), 거래소별=res, 나쁜쪽=worse,
견딜선_안=bool(-worse["규칙"]["최대낙폭"] <= T))
def rolling(coin, Ts=(0.10, 0.20, 0.30), ks=(1.0, 0.8), years=2):
"""반년마다 새로 시작해 2년씩 — 그 날까지의 이력으로 비중을 정하고 앞 2년 계좌 낙폭이 견딜 선 안이었나."""
cfg = BOTS[coin]
per, st = series(cfg)
bases = []
y, m = 2019, 7
while True:
b = f"{y}-{m:02d}-01"
e = f"{y + years}-{m:02d}-01"
if e > ASOF[:10]:
break
bases.append((b, e))
m += 6
if m > 12:
y += 1; m -= 12
rows = []
for b, e in bases:
S0 = per["업비트"][0]
i_first = WARM + 6 * 365 * 1 # 이력 최소 1년(워밍업 뒤)
if int(np.searchsorted(S0["d"], b)) < i_first:
continue
M, Mm = worst_M(per, b)
for T in Ts:
for k in ks:
x = view_x(k, T, M)
Mh, _ = worst_M_hold(per, b); xh = view_x(k, T, Mh)
dd_h = {}; fin_h = {}
for mk, (S, pos) in per.items():
a = int(np.searchsorted(S["d"], b)); z = int(np.searchsorted(S["d"], e))
eqh = hold_rule(S, a, z, xh, rebalance_bars=6 * 365)
dd_h[mk] = mdd(eqh); fin_h[mk] = eqh[-1]
dd = {}; dd_rb = {}; dd_bot = {}; dd_sep = {}; fin = {}; fin_bot = {}; fin_hold = {}
for mk, (S, pos) in per.items():
a = int(np.searchsorted(S["d"], b)); z = int(np.searchsorted(S["d"], e))
acc = accounts(S, pos, a, z, x)
dd[mk] = mdd(acc["규칙"]); dd_rb[mk] = mdd(acc["규칙_매봉맞춤"]); dd_bot[mk] = mdd(acc["전부봇"])
dd_sep[mk] = mdd(acc["규칙_따로둠"])
fin[mk] = acc["규칙"][-1]; fin_bot[mk] = acc["전부봇"][-1]; fin_hold[mk] = acc["존버"][-1]
rows.append(dict(기준일=b, 끝=e, T=T, k=k, M=round(M, 4), x=round(x, 4),
계좌낙폭=round(min(dd.values()), 4), 따로둠낙폭=round(min(dd_sep.values()), 4),
매봉맞춤낙폭=round(min(dd_rb.values()), 4), 전부봇낙폭=round(min(dd_bot.values()), 4),
계좌끝=round(min(fin.values())), 전부봇끝=round(min(fin_bot.values())), 존버끝=round(min(fin_hold.values())),
존버공식_비중=round(xh, 4), 존버공식_낙폭=round(min(dd_h.values()), 4), 존버공식_끝=round(min(fin_h.values())),
존버공식_지킴=bool(-min(dd_h.values()) <= T), 봇공식이_더남김=bool(min(fin.values()) > min(fin_h.values())),
지킴=bool(-min(dd.values()) <= T), 지킴_따로둠=bool(-min(dd_sep.values()) <= T),
지킴_매봉맞춤=bool(-min(dd_rb.values()) <= T),
전부봇_지킴=bool(-min(dd_bot.values()) <= T)))
summ = {}
for k in ks:
r = [q for q in rows if q["k"] == k]
summ[f"k{k}"] = dict(판=len(r), 지킴=sum(q["지킴"] for q in r), 지킴_따로둠=sum(q["지킴_따로둠"] for q in r),
지킴_매봉맞춤=sum(q["지킴_매봉맞춤"] for q in r),
전부봇_지킴=sum(q["전부봇_지킴"] for q in r),
존버공식_지킴=sum(q["존버공식_지킴"] for q in r), 봇공식이_더남김=sum(q["봇공식이_더남김"] for q in r),
어긴판=[(q["기준일"], q["T"], q["계좌낙폭"]) for q in r if not q["지킴"]])
return dict(코인=coin, 공통시작=st[:10], 기준일수=len({q["기준일"] for q in rows}), 요약=summ, 판=rows)
def worst_after(coin="비트코인", start="2019-03-01"):
"""start 뒤로 봇에 100% 넣은 계좌의 최대 낙폭 — 2018 하락장 뒤로 그만큼 잃은 적이 있었나."""
per, st = series(BOTS[coin])
out = {}
for mk, (S, pos) in per.items():
i0 = int(np.searchsorted(S["d"], start))
eq = np.cumprod(np.r_[1.0, bot_growth(S, pos)[i0:]])
d = S["d"][i0 - 1:]
pk = np.maximum.accumulate(eq); dd = eq / pk - 1; k = int(np.argmin(dd)); j = int(np.argmax(eq[:k + 1]))
out[mk] = dict(최대낙폭=round(float(dd.min()), 4), 고점날=d[j][:10], 바닥날=d[k][:10])
return out
def hold_rule_sweep(coin="비트코인", k=K):
"""반론 — 봇 없이 그냥 비트코인을 같은 공식(보유의 과거 최악 손실)으로 사면? 다시 맞추는 주기를 바꿔 가며."""
r = rolling(coin, ks=(k,))
bases = sorted({q["기준일"]: q["끝"] for q in r["판"]}.items())
per, st = series(BOTS[coin])
out = {}
for reb, lab in ((0, "안 맞춤"), (BPD * 30, "매달"), (BPD * 91, "분기마다"), (BPD * 365, "해마다"), (1, "매 봉")):
bad = 0; win = 0; n = 0
for b, e in bases:
Mh, _ = worst_M_hold(per, b)
for T in (0.10, 0.20, 0.30):
xh = view_x(k, T, Mh); dd = []; fin = []
for mk, (S, pos) in per.items():
a = int(np.searchsorted(S["d"], b)); z = int(np.searchsorted(S["d"], e))
eq = hold_rule(S, a, z, xh, rebalance_bars=reb); dd.append(mdd(eq)); fin.append(eq[-1])
q = [x for x in r["판"] if x["기준일"] == b and x["T"] == T][0]
n += 1; bad += int(-min(dd) > T); win += int(q["계좌끝"] > min(fin))
out[lab] = dict(판=n, 견딜선_넘음=bad, 봇공식이_더남김=win)
out["봇공식_견딜선_넘음"] = r["요약"][f"k{k}"]["판"] - r["요약"][f"k{k}"]["지킴"]
return out
def regression():
"""연구 풀 an7b(2019-09-01 시작 · 매봉맞춤 · 2026-09-30 끝) 재현 대조."""
cfg = BOTS["비트코인"]
per = {}
for mk, sym in (("업비트", "upbit_KRW-BTC"), ("바이낸스", "binance_BTCUSDT")):
S = load(os.path.join(DATA, sym + "_4h.csv"), start="2019-09-01T00:00:00", end="2026-09-30T23:59:59")
per[mk] = (S, bot_pos(S, cfg))
M, Mm = worst_M(per, HOLD_START)
x = 0.8 * 0.2 / M
S, pos = per["바이낸스"]
a = int(np.searchsorted(S["d"], HOLD_START))
rb = SEED * np.cumprod(rebal_growth(S, x * pos)[a:])
return dict(M=round(M, 4), M_거래소별={m: round(v, 4) for m, v in Mm.items()}, x=round(x, 4),
바이낸스_매봉맞춤_끝=round(float(rb[-1])), 바이낸스_매봉맞춤_낙폭=round(mdd(rb), 4),
연구값=dict(M=0.3573, x=0.4478, 끝=12471999, 낙폭=-0.133))
def 만(v):
return f"{round(v / 1e4):,}만원"
def 퍼(v):
return f"{abs(v) * 100:.1f}%"
def main():
for coin, cfg in BOTS.items():
for mk, sym in (("업비트", "upbit_" + cfg["up"]), ("바이낸스", "binance_" + cfg["bi"])):
S = load(os.path.join(DATA, sym + "_4h.csv"))
g = gaps(S)
print(f"[자료] {coin} {mk}: {S['n']:,}봉 · {S['d'][0][:16]} ~ {S['d'][-1][:16]} · 4시간 넘게 빈 자리 {g[0]}곳(가장 긴 {g[1]:.0f}시간)")
h = holdout("비트코인", 0.20)
w = h["나쁜쪽"]
print()
print(f"[본편 · 비트코인 봇 · 1,000만원 · 참을 손실 20% · 공식 정한 날 {h['기준일']} · 두 거래소 공통 시작 {h['공통시작']}]")
print(f" 봇의 과거 최악 손실(2017-12 ~ 2023-12): 업비트 {퍼(h['M_거래소별']['업비트'])} · 바이낸스 {퍼(h['M_거래소별']['바이낸스'])} → 큰 쪽 {퍼(h['M'])} (정수로 {round(h['M'] * 100)}%)")
print(f" 주문 크기 = 20 × 0.8 ÷ {round(h['M'] * 100)} → 계좌 전체의 {round(h['봇비중'] * 100)}%")
wa = worst_after("비트코인")
print(" 2018년 하락장 뒤(2019-03 ~)로 봇에 전부 넣은 계좌가 가장 크게 줄어든 폭: "
+ " · ".join(f"{mk} {퍼(v['최대낙폭'])}({v['고점날']} → {v['바닥날']})" for mk, v in wa.items()))
for lab, name in (("전부봇", "봇에 전부"), ("규칙", "공식대로"), ("존버", "비트코인 그냥 보유"),
("규칙_따로둠", "공식 비중을 처음에 떼어 두고 그대로"), ("규칙_매봉맞춤", "공식 비중을 매 봉 다시 맞춤(연구 판)")):
print(f" {name}: 가장 크게 줄어든 폭 {퍼(w[lab]['최대낙폭'])} · 끝 {만(w[lab]['끝'])}")
for mk, r in h["거래소별"].items():
if mk.startswith("_"):
continue
print(f" [{mk} {r['구간']}] 봇이 들고 있던 시간 {r['봇보유비율'] * 100:.0f}% · 사고판 횟수 {r['매매횟수']}번")
for lab, name in (("전부봇", "봇에 전부"), ("규칙", "공식대로"), ("존버", "그냥 보유")):
v = r[lab]
print(f" {name}: 고점 {만(v['고점금액'])}({v['고점날']}) → 바닥 {만(v['바닥금액'])}({v['바닥날']}) · 줄어든 폭 {퍼(v['최대낙폭'])} · 끝 {만(v['끝'])}")
print(f" 공식대로 한 계좌가 참을 선(20%) 안: {'예' if h['견딜선_안'] else '아니오'}")
print()
print("[참을 손실을 바꾸면 · 같은 기간]")
for T in (0.10, 0.20, 0.30):
v = holdout("비트코인", T)
print(f" 참을 손실 {round(T * 100)}% → 주문 크기 {round(v['봇비중'] * 100)}% · 가장 크게 줄어든 폭 {퍼(v['나쁜쪽']['규칙']['최대낙폭'])} · 끝 {만(v['나쁜쪽']['규칙']['끝'])}")
v = holdout("비트코인", 0.20, k=1.0)
print(f" (0.8 없이 · 계수 1.0) 주문 크기 {round(v['봇비중'] * 100)}% · 가장 크게 줄어든 폭 {퍼(v['나쁜쪽']['규칙']['최대낙폭'])}")
print()
for coin in ("비트코인", "이더리움"):
r = rolling(coin)
print(f"[다른 때 시작해도 · {coin} 봇 · 2019-07 ~ 2024-07 반년마다 시작한 2년 계좌 {r['기준일수']}개 × 참을 손실 10·20·30%]")
for k in (0.8, 1.0):
s = r["요약"][f"k{k}"]
print(f" 계수 {k}: {s['판']}번 중 참을 선을 넘은 경우 — 공식대로(주문 크기) {s['판'] - s['지킴']}번 · 처음에 떼어 두고 그대로 {s['판'] - s['지킴_따로둠']}번 · 매 봉 맞춤 {s['판'] - s['지킴_매봉맞춤']}번 · 봇에 전부 {s['판'] - s['전부봇_지킴']}번")
s = r["요약"]["k0.8"]
if s["어긴판"]:
print(" 공식대로 한 계좌가 넘은 경우: " + " · ".join(f"{b} 시작·참을 {round(T * 100)}%·{퍼(d)}" for b, T, d in s["어긴판"]))
print()
hs = hold_rule_sweep("비트코인")
print("[반론 · 봇 없이 비트코인을 같은 공식으로 사면(보유의 과거 최악 손실로 비중) · 같은 33번]")
for lab in ("안 맞춤", "매달", "분기마다", "해마다", "매 봉"):
v = hs[lab]
print(f" 다시 맞추는 주기 {lab}: 참을 선을 넘은 경우 {v['견딜선_넘음']}번 · 끝 금액이 봇 공식 쪽이 더 많은 경우 {v['봇공식이_더남김']}번")
print(f" 봇 공식(주문 크기)이 넘은 경우: {hs['봇공식_견딜선_넘음']}번")
he = holdout("비트코인", 0.20)["거래소별"]["_존버공식"]
print(f" 비트코인 보유의 과거 최악 손실 {퍼(he['M'])} → 같은 공식 비중 {round(he['비중'] * 100)}%")
print()
e = holdout("이더리움", 0.20)
we = e["나쁜쪽"]
print(f"[한계 · 이더리움 봇(이동평균 20/200 · 일봉 50일선) · 2024-01 ~ · 참을 손실 20%] 과거 최악 손실 {퍼(e['M'])} → 주문 크기 {round(e['봇비중'] * 100)}%")
for lab, name in (("전부봇", "봇에 전부"), ("규칙", "공식대로"), ("존버", "이더리움 그냥 보유")):
print(f" {name}: 가장 크게 줄어든 폭 {퍼(we[lab]['최대낙폭'])} · 끝 {만(we[lab]['끝'])}")
print()
print("끝.")
if __name__ == "__main__":
main()
알고랩 Strategy Lab — 전략을 검증하고, 검증한 것만 자동매매로 옮깁니다. 주문 크기를 참을 손실에 맞춰 거는 일까지 봇에 넣어 드립니다. 알고랩 자동매매 살펴보기 · 전략 검증 전체 보기
과거 데이터를 돌려 본 결과이며 미래 수익을 보장하지 않습니다. 투자 판단과 책임은 본인에게 있습니다.