비트코인 봇 · 자금 관리 · 주문 크기 · 실측 재현 페이지

비트코인 봇에 넣을 돈은
이렇게 정하세요

봇에 돈을 전부 넣으면 떨어질 때 계좌도 그만큼 줄어듭니다. 그래서 봇이 한 번 살 때 쓰는 돈을 내가 참을 수 있는 손실에 맞춰 정하는 공식을 실제 가격으로 쟀습니다. 업비트 원화와 바이낸스 USDT 4시간봉을 각각 돌려 나쁜 쪽 숫자를 씁니다.

28.0%2024년 1월 1,000만원 · 봇에 전부 넣은 계좌가 가장 크게 줄어든 폭(바이낸스 2024-03-04 1,356만원 → 2024-09-18 977만원)
11.1%같은 봇 · 주문 크기를 계좌 전체의 33%로 건 계좌가 가장 크게 줄어든 폭 (참을 손실 20% 안)
33번 중 0번2019년 7월~2024년 7월 반년마다 시작한 2년 계좌 × 참을 손실 10·20·30% 에서 공식대로 한 계좌가 참을 선을 넘은 경우

오늘 드리는 것 — 공식 한 줄

항목내용
공식봇 주문 크기(계좌 전체의 %) = 참을 손실 × 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 을 곱해도 넘은 때가 있었습니다

실측 — 2024년 1월 1일 ~ 2026년 10월 13일 · 1,000만원 · 참을 손실 20%

계좌가장 크게 줄어든 폭끝 금액
봇에 전부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 — 전략을 검증하고, 검증한 것만 자동매매로 옮깁니다. 주문 크기를 참을 손실에 맞춰 거는 일까지 봇에 넣어 드립니다. 알고랩 자동매매 살펴보기 · 전략 검증 전체 보기

과거 데이터를 돌려 본 결과이며 미래 수익을 보장하지 않습니다. 투자 판단과 책임은 본인에게 있습니다.