전략 발굴 · 전수 백테스트

전략 1,887개를 걸렀더니
1000만원이 5억 4천만원이 됐습니다.

2017년 12월부터 2026년 8월까지, 업비트 4시간봉에서 비트코인과 이더리움을 자동매매한 백테스트입니다. 트레이딩뷰 인기 전략 13종에 자산 13개·시간봉 3개·설정값을 조합한 후보 1,887개를 전부 돌렸고, 연복리 30% 이상 · 최대낙폭 30% 이내를 업비트와 바이낸스 두 거래소에서 동시에 통과한 조합 하나가 남았습니다. 규칙·설정값·재현 코드를 전부 공개합니다.

재현 코드 받기 이 계산의 한계 →
54.3배
1000만원 → 5억 4341만원 (8.7년)
연복리 58.0%
최대낙폭 -29.6% (업비트 원화)
연복리 41.7%
최대낙폭 -29.4% (바이낸스 USDT)

1. 어떻게 걸렀나

기준은 두 가지 — 연복리 수익률 30% 이상, 최대낙폭 30% 이내 (비용 차감 후, 전 구간).

단계내용통과
전수 시험전략 13종 × 자산 13개 × 시간봉 3개 × 설정값 = 1,887개
1차 결과수익률 기준만 통과 445개 · 낙폭 기준만 통과 137개0개
조건 한 줄 추가가격이 일봉 이동평균선 위에 있을 때만 진입(하락장 차단)185개
교차시장 검증업비트 그리고 바이낸스에서 모두 기준을 넘는 설정만최종 1조합

업비트 단일 기준 1위(연복리 89.8%)는 버렸습니다. 같은 규칙이 바이낸스에서는 29.0%로 떨어졌기 때문입니다 — 두 시장의 포지션이 95.5% 일치하는데 수익이 3배 넘게 갈린다는 건, 수익이 소수의 급등 구간에 몰려 있어 요행에 가깝다는 뜻입니다. 후보를 1,887개씩 돌리면 이런 요행 1등이 반드시 나오므로, 걸러내는 장치가 필요합니다.

2. 살아남은 조합의 규칙

전부 공개합니다. 숨길 게 없어야 검증도 됩니다.

📈

켈트너 채널 돌파

4시간봉에서 가격이 [EMA n + k × ATR n] 상단을 넘으면 전액 매수, EMA 아래로 내려오면 전량 매도(현금). 숏 없음.
설정: 비트코인 n=50, k=1.25 · 이더리움 n=60, k=2.0

🧭

일봉 100일선 필터

가격이 일봉 100일 이동평균선(= 4시간봉 600개 평균) 위에 있을 때만 진입. 큰 흐름이 하락이면 아예 들어가지 않습니다. 이더리움이 85.4% 무너진 2018년을 이 조합은 -10.3%로 지나갔습니다.

⚖️

비트코인 · 이더리움 반반

자금을 반씩 나눠 각자의 설정으로 돌립니다. 2017-12~2026-08 105개월 중 27개월은 매매 없이 현금으로 대기했습니다.

🧾

비용 · 체결 가정

비중이 바뀔 때마다 편도 0.1%(수수료+슬리피지) 차감. 신호는 봉 종가 확정 후 다음 봉부터 반영(미래참조 방지). 편도 0.3%로 올려도 기준을 유지합니다.

3. 성적표

같은 규칙, 두 거래소. 기준일 2026-08-31 고정.

구간업비트 원화바이낸스 USDT
전 구간 (2017-12 ~ 2026-08)연복리 +58.0% · 낙폭 -29.6%연복리 +41.7% · 낙폭 -29.4%
2024-01 ~ 2026-08 만 잘라서연복리 +40.8% · 낙폭 -17.1%연복리 +19.0% · 낙폭 -23.4%
1000만원이라면5억 4341만원2억 1882만원
연도201820192020202120222023202420252026
이 조합 (업비트)-10%+138%+214%+154%-6%+36%+101%+18%+4%
그냥 반반 보유-82%+45%+368%+227%-65%+135%+103%-9%-21%

그냥 보유도 8.7배가 됐던 시장입니다. 이 조합의 값어치는 수익률만이 아니라 -82%, -65% 해를 -10%, -6%로 지나간 것 — 계좌가 견딜 수 있는 낙폭 안에서 54.3배를 만든 데 있습니다.

4. 이 계산의 한계 — 접지 말고 읽으세요

① 설정값은 과거 데이터 전체에서 골랐습니다. 과최적화에서 완전히 자유롭지 않습니다. 그래서 개발에 덜 물든 2024년 이후 구간과 두 거래소 교차 검증을 함께 공개합니다.
② 과거 9년에는 2020~21 암호화폐 초강세장이 포함돼 있습니다. 미래 수익을 보장하지 않습니다.
③ 4시간봉 종가 체결·편도 0.1% 비용을 가정한 시뮬레이션입니다. 실제 체결가는 다를 수 있습니다.
④ 세금(해외 거래소 양도차익 등)은 계산에 넣지 않았습니다.
⑤ 거래소가 과거 캔들을 미세 보정하는 경우가 있어, 재현 시 배수의 마지막 자리에서 0.1~0.2 수준의 차이가 날 수 있습니다(연복리·낙폭은 일치).

5. 재현 코드 — 믿지 말고 직접 돌려보세요

업비트·바이낸스 공개 API 에서 데이터를 직접 받아 위 숫자를 그대로 재현합니다. API 키 불필요.

파이썬 설치python.org에서 3.8 이상. 설치 때 Add to PATH 체크.

numpy 설치 — 터미널에서 pip install numpy

실행 — 아래 코드를 받아 python discovery_backtest.py. 첫 실행은 캔들 수집에 3~5분 걸리고, 같은 폴더에 캐시되어 두 번째부터는 몇 초입니다.

discovery_backtest.py 파일 받기
# -*- coding: utf-8 -*-
# =============================================================================
# AlgoLab · 전략 1,887개 발굴 백테스트 — 공개용
#   영상 「1,000만원 코인 자동매매, 1,887개 백테스트」의 최종 조합 숫자를 그대로 재현합니다.
#   믿지 말고 직접 돌려보세요. 설정값을 바꿔 여러분의 조건으로 실험해도 됩니다.
#
# 실행:  python discovery_backtest.py
# 필요:  Python 3.8+ / numpy  (pip install numpy)
#        데이터는 업비트·바이낸스 공개 API 에서 자동으로 받습니다(API 키 불필요).
#        첫 실행은 캔들 수집에 3~5분 걸립니다(같은 폴더에 CSV 로 캐시되어 두 번째부터는 몇 초).
#
# ── 최종 조합 (영상 그대로) ──────────────────────────────────────────────────
#   · 4시간봉 켈트너 채널 돌파: 가격이 [EMA n + k x ATR n] 상단을 넘으면 전액 매수,
#     EMA 아래로 내려오면 전량 매도(현금). 숏 없음.
#   · 추세 필터: 가격이 일봉 100일 이동평균선(= 4시간봉 600개 SMA) 위에 있을 때만 진입.
#   · 설정: 비트코인 n=50, k=1.25 / 이더리움 n=60, k=2.0. 자금은 반반, 봉마다 리밸런스 가정.
#   · 비용: 비중이 바뀔 때마다 편도 0.1% (업비트 수수료 0.05% + 슬리피지 가정).
#   · 신호는 봉 종가로 확정하고 **다음 봉부터** 반영합니다(미래참조 방지).
#
# ── 기준일(ASOF) 고정 ────────────────────────────────────────────────────────
#   ASOF 이후 데이터를 자르지 않으면 실행 시점마다 숫자가 달라져 영상과 어긋납니다.
#   최신까지 보고 싶으면 ASOF = None 으로 바꾸세요(영상 숫자와는 달라집니다).
#
# ── 이 계산의 한계 (영상에서도 밝힙니다) ─────────────────────────────────────
#   · 설정값은 과거 데이터 전체(2017-12~2026-08)에서 골랐습니다. 과최적화에서
#     완전히 자유롭지 않고, 미래 수익을 보장하지 않습니다.
#   · 4시간봉 종가 체결을 가정합니다. 실제 체결가는 다를 수 있습니다.
#   · 과거 9년에는 2020~21 암호화폐 초강세장이 포함되어 있습니다.
#   · 거래소가 과거 캔들을 미세 보정하는 경우가 있어, 배수의 마지막 자리에서
#     0.1~0.2 수준의 차이가 날 수 있습니다(예: 54.3배 → 54.4배). 연복리·낙폭은 일치합니다.
# =============================================================================
import csv, datetime, json, os, sys, time, urllib.parse, urllib.request

import numpy as np

try:                                             # 윈도우 기본 콘솔(cp949)에서도 죽지 않게
    sys.stdout.reconfigure(encoding="utf-8", errors="replace")
except Exception:
    pass

ASOF = "2026-08-31T12:00:00"        # UTC. 이 봉(포함)까지 사용 — 영상 제작 시점과 동일
COST = 0.001                        # 편도 0.1%
HERE = os.path.dirname(os.path.abspath(__file__))


# ── 데이터 수집 (캐시 → 없으면 API. ASOF 자르기는 양쪽에 똑같이 적용) ────────
def _get(url):
    req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0",
                                               "Accept": "application/json"})
    return json.loads(urllib.request.urlopen(req, timeout=30).read().decode())


def fetch_upbit(market):
    rows, to = [], ""
    while True:
        url = ("https://api.upbit.com/v1/candles/minutes/240?market=" + market +
               "&count=200" + ("&to=" + urllib.parse.quote(to) if to else ""))
        batch = _get(url)
        if not batch:
            break
        for b in batch:
            rows.append((b["candle_date_time_utc"], b["opening_price"],
                         b["high_price"], b["low_price"], b["trade_price"]))
        to = batch[-1]["candle_date_time_utc"]
        time.sleep(0.12)
    rows.reverse()
    return rows


def fetch_binance(symbol):
    rows, start = [], int(datetime.datetime(2017, 8, 1).timestamp() * 1000)
    while True:
        url = ("https://api.binance.com/api/v3/klines?symbol=" + symbol +
               "&interval=4h&limit=1000&startTime=" + str(start))
        batch = _get(url)
        if not batch:
            break
        for b in batch:
            t = datetime.datetime.utcfromtimestamp(b[0] / 1000).strftime("%Y-%m-%dT%H:%M:%S")
            rows.append((t, b[1], b[2], b[3], b[4]))
        start = batch[-1][0] + 1
        if len(batch) < 1000:
            break
        time.sleep(0.15)
    return rows


def load(tag, fetch):
    path = os.path.join(HERE, "cache_" + tag + ".csv")
    if not os.path.exists(path):
        print("  " + tag + " 수집 중…")
        rows = fetch()
        with open(path, "w", newline="", encoding="utf-8") as f:
            w = csv.writer(f)
            w.writerow(["date", "open", "high", "low", "close"])
            w.writerows(rows)
    ts, o, h, l, c = [], [], [], [], []
    for r in csv.DictReader(open(path, encoding="utf-8")):
        if ASOF and r["date"] > ASOF:          # ★ 캐시든 신규든 같은 기준으로 자른다
            continue
        ts.append(r["date"])
        o.append(float(r["open"])); h.append(float(r["high"]))
        l.append(float(r["low"])); c.append(float(r["close"]))
    return ts, np.array(o), np.array(h), np.array(l), np.array(c)


# ── 지표 ─────────────────────────────────────────────────────────────────────
def ema(x, n):
    a = 2.0 / (n + 1)
    out = np.empty(len(x)); out[0] = x[0]
    for i in range(1, len(x)):
        out[i] = a * x[i] + (1 - a) * out[i - 1]
    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)))
    out = np.full(len(tr), np.nan)
    if len(tr) >= n:
        out[n - 1] = tr[:n].mean()
        for i in range(n, len(tr)):
            out[i] = out[i - 1] + (tr[i] - out[i - 1]) / n
    return out


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


# ── 전략: 켈트너 돌파 + 일봉 100일선 필터 ────────────────────────────────────
# 켈트너 '상태'(돌파~이탈)와 필터를 따로 두고 곱한다. 필터 때문에 나갔다가
# 가격이 100일선 위로 돌아오면, 켈트너 상태가 살아 있는 한 다시 들어간다.
def signal(o, h, l, c, n, k):
    e = ema(c, n); a = atr(h, l, c, n)
    f100 = sma(c, 600)                          # 4시간봉 600개 = 100일
    sig = np.zeros(len(c)); base = 0.0
    for i in range(len(c)):
        if np.isnan(a[i]):
            sig[i] = 0.0; continue
        if c[i] > e[i] + k * a[i]:              # 돌파 → 상태 ON (같은 봉에선 매수 우선)
            base = 1.0
        elif c[i] < e[i]:                        # EMA 이탈 → 상태 OFF
            base = 0.0
        ok = (not np.isnan(f100[i])) and c[i] > f100[i]
        sig[i] = base if ok else 0.0
    if len(sig) > n:
        sig[:n + 1] = 0.0                        # 워밍업
    return sig


def growth(c, sig):
    ret = np.zeros(len(c)); ret[1:] = c[1:] / c[:-1] - 1
    pos = np.r_[0.0, sig[:-1]]                  # 다음 봉 반영
    return pos, (1 + pos * ret) * (1 - COST * np.abs(np.diff(np.r_[0.0, pos])))


def stats(g, ts):
    eq = np.cumprod(g)
    yrs = (datetime.date.fromisoformat(ts[-1][:10])
           - datetime.date.fromisoformat(ts[0][:10])).days / 365.25
    mdd = float((eq / np.maximum.accumulate(eq) - 1).min())
    return float(eq[-1] ** (1 / yrs) - 1), mdd, float(eq[-1])


def run(tag, btc, eth):
    tb, ob, hb, lb, cb = btc
    te, oe, he, le, ce = eth
    WARM = 400
    _, gb = growth(cb, signal(ob, hb, lb, cb, 50, 1.25))
    _, ge = growth(ce, signal(oe, he, le, ce, 60, 2.0))
    ib_map = {t: i for i, t in enumerate(tb)}
    pairs = [(ib_map[t], i) for i, t in enumerate(te) if t in ib_map]
    ib = np.array([x[0] for x in pairs]); ie = np.array([x[1] for x in pairs])
    m = (ib >= WARM) & (ie >= WARM)
    gc = 0.5 * gb[ib[m]] + 0.5 * ge[ie[m]]
    dl = [te[i] for i in ie[m]]
    cagr, mdd, mult = stats(gc, dl)
    print("\n[" + tag + "]  " + dl[0][:10] + " ~ " + dl[-1][:10])
    print("  반반 조합: 연복리 {:+.1%} · 최대낙폭 {:+.1%} · {:.1f}배 (1000만원 → {:,.0f}만원)"
          .format(cagr, mdd, mult, mult * 1000))
    yr = {}
    for i, t in enumerate(dl):
        yr[t[:4]] = yr.get(t[:4], 1.0) * gc[i]
    print("  연도별: " + " · ".join(y + " {:+.0f}%".format((v - 1) * 100)
                                  for y, v in sorted(yr.items())))


if __name__ == "__main__":
    print("전략 1,887개 발굴 편 — 최종 조합 재현 (기준일 " + str(ASOF) + " UTC)")
    up_btc = load("upbit_btc", lambda: fetch_upbit("KRW-BTC"))
    up_eth = load("upbit_eth", lambda: fetch_upbit("KRW-ETH"))
    run("업비트 원화", up_btc, up_eth)
    try:
        bn_btc = load("binance_btc", lambda: fetch_binance("BTCUSDT"))
        bn_eth = load("binance_eth", lambda: fetch_binance("ETHUSDT"))
        run("바이낸스 USDT", bn_btc, bn_eth)
    except Exception as e:
        print("\n[바이낸스] 수집 실패(" + str(e) + ") — 지역에 따라 API 가 막힐 수 있습니다. 업비트 결과만 확인하세요.")
    print("\n영상 수치: 업비트 +58.0% / -29.6% / 54.3배 · 바이낸스 +41.7% / -29.4%")

이 규칙, 사람이 직접 지킬 수 있을까요

신호는 4시간마다, 한밤중에도 나옵니다. 105개월 중 27개월은 아무것도 안 하고 기다려야 했고, 기다리다 신호가 켜지면 새벽이라도 사야 합니다. 이걸 8년 넘게 손으로 하는 사람은 없습니다 — 이게 자동매매가 필요한 이유입니다.

직접 만들어 보실 분은 위 코드에서 출발하시면 됩니다. 구현·검증까지 맡기고 싶으시면 알고랩이 여러분의 전략을 봇으로 만들어 드립니다. 상담과 견적은 무료입니다.

자동매매 제작 문의하기 완성형 리밸런싱봇 보기 →
Strategy Lab · 이 검증 더 보기
이 검증을 영상으로 보기4분 42초 낙폭 −53% 전략 — 장치 22개 중 살린 건 1개이 전략의 후속 검증 인기 지표 13종 888조합 검증 — 살아남은 4개지표 검증 전략 검증 전체 보기 →

※ 본 페이지의 수치는 공개된 시장 데이터를 과거 기준으로 검증한 교육·검증 목적의 자료이며, 특정인에 대한 투자권유나 매수·매도 권유가 아닙니다. 과거 백테스트 성과가 미래 수익을 보장하지 않습니다. 설정값은 과거 데이터에서 선택되어 과최적화 위험이 있으며, 암호화폐는 원금 전액 손실이 가능한 고위험 자산입니다. 모든 투자 판단과 그에 따른 책임은 이용자 본인에게 있습니다.