ALGOLAB 검증 노트 — 트레이딩뷰 인기 지표 888조합

같은 코인, 지표만 바꿨더니
1000만원이 6억과 5만원이 됐습니다

트레이딩뷰에서 많이 쓰이는 지표 13종을 설정값·일봉 필터까지 바꿔 888개 조합으로 만들고, 업비트 원화와 바이낸스 달러 두 거래소 9년(2017-12~2026-08)에서 전부 백테스트했습니다(1,776번). 잣대(두 거래소 모두 연복리 30% 이상 · 최대낙폭 -35% 이내 · 편도 0.1% 비용)를 통과한 조합은 4개였습니다. 이 페이지에서 그 4개의 설정값, 트레이딩뷰 지표 파일, 재현 코드를 전부 가져가실 수 있습니다.

888 → 4
조합 → 잣대 통과
6억 1172만 vs 5만원
1000만원 · 같은 업비트 BTC 9년
1,776번
백테스트 (두 거래소 × 888)
트레이딩뷰 지표 받기 재현 코드 보기

잣대 통과 4개 — 설정값 전부

배수는 1,000만원 기준. 검증 = 2024년 이후만 자른 구간의 연복리.

조합업비트 연복리낙폭배수검증 바이낸스 연복리낙폭배수검증
UT Bot (key 2.0 · atr_n 21 · SMA50)60.1%-26.0%61.2배33.7%39.5%-34.7%19.1배10.9%
UT Bot (key 3.0 · atr_n 7 · SMA200)49.6%-32.7%33.8배37.6%35.8%-34.0%15.1배20.3%
슈퍼트렌드 (n 7 · mult 2.5 · SMA200)53.2%-34.8%41.8배50.4%34.5%-34.7%13.8배19.1%
켈트너 (n 50 · k 2.0 · SMA200)48.0%-26.0%30.8배31.3%30.8%-33.9%10.8배14.0%
그냥 보유 (참고)28.0% -87.0%8.6배 업비트 BTC 같은 구간
수익률 관문(min 30%)만 통과한 조합은 422개였습니다. 벌 줄 아는 지표는 널렸고, 낙폭(-35%)까지 버틴 게 4개입니다. 최고 수익 탈락자는 이동평균 크로스 10·200 — 이더리움에서 490.8배를 벌고도 낙폭 -54.8%로 떨어졌습니다.
단일 거래소 착시 — 슈퍼트렌드 n20/mult3.5+SMA100 은 업비트에서 51.3배(낙폭 -30.0%)로 통과처럼 보이지만, 같은 조합이 바이낸스에서는 8.7배(낙폭 -46.8%)로 무너집니다. 한 거래소 성적은 실력이 아니라 그 시장의 운일 수 있어, 잣대에 '두 거래소 동시 통과'를 넣었습니다.

살아남은 건 이름이 아니라 구조였습니다

전부 비트코인

이더리움 조합은 수익은 컸지만 낙폭이 번번이 잣대를 넘었습니다. 4개 전부 BTC.

전부 일봉 추세필터

종가가 일봉 이동평균 위일 때만 진입하는 조건 한 줄. 생존 슈퍼트렌드는 2018·2022 하락장 수익률이 정확히 0 — 그 해에 거래 자체를 안 했습니다.

📈

전부 변동폭 밴드 추종

ATR 밴드·채널을 따라가는 추세추종 계열만 남았습니다. RSI·볼린저 같은 평균회귀 192개 조합은 전멸.

마법의 숫자가 아닙니다

생존 조합도 바로 옆 설정값은 아슬아슬하게 떨어졌습니다. 비용을 2배(편도 0.2%)로 올려도 연 44~50%는 유지했지만, 잣대가 빡셀 뿐 과신은 금물입니다.

한계 — 접지 않고 그대로 둡니다

① 과거 9년 전체에서 고른 최적값이며 시뮬레이션입니다. 미래를 보장하지 않습니다.
② 검증 구간(2024~)은 업비트 연 31~50%, 바이낸스 연 11~20%로 낮아집니다.
③ 4시간봉 종가 체결 · 편도 0.1% 가정, 슬리피지 미반영.
④ 영상 데이터는 마지막 봉 확정 전에 수집돼, 재현 시 바이낸스 검증 일부 수치가 ±0.3%p 안에서 다를 수 있습니다. 본편의 배수·낙폭 등 핵심 수치는 재수집 데이터로 자릿수까지 일치함을 확인했습니다(2026-09-02).

트레이딩뷰 지표 파일 — 4개 세팅, 신호+알림

위 4개 조합의 진입·청산 신호를 차트에 그대로 그립니다. 무료입니다.

트레이딩뷰에서 BTC 4시간봉 차트를 엽니다(업비트 KRW-BTC 또는 바이낸스 BTCUSDT). 다른 시간봉에서는 검증과 다른 신호가 나옵니다.

Pine 에디터를 열고 아래 코드를 붙여넣은 뒤 차트에 추가.

알림은 봉 마감 시 1회로 설정하세요. 세팅별 진입·청산 알림 8종이 들어 있습니다.

algolab_verified4.pine
파일 받기
//@version=5
// ============================================================================
// AlgoLab Verified 4 — BTC 4H  (구독자 배포판 v1 · 2026-09-02)
//
// 트레이딩뷰 인기 지표 13종 × 888개 조합을 업비트(원화)+바이낸스(USDT) 9년으로
// 전부 백테스트해서, 두 시장 동시 잣대(연복리 30% 이상 · 최대낙폭 -35% 이내 ·
// 편도 비용 0.1%)를 통과한 조합은 4개뿐이었습니다. 이 지표는 그 4개의 신호를
// 그대로 그립니다. 재현 코드·전체 수치: https://algolab.co.kr
//
//  1) UT Bot  key 2.0 / ATR 21  + 일봉 SMA50 필터   (업비트 61.2배 · 바이낸스 19.1배)
//  2) UT Bot  key 3.0 / ATR 7   + 일봉 SMA200 필터  (33.8배 · 15.1배)
//  3) 슈퍼트렌드 n 7 / mult 2.5  + 일봉 SMA200 필터  (41.8배 · 13.8배)
//  4) 켈트너   n 50 / k 2.0     + 일봉 SMA200 필터  (30.8배 · 10.8배)
//
// ⚠ 반드시 읽을 것
//  · BTC/KRW 또는 BTC/USDT **4시간봉 차트 전용**입니다. 일봉 필터를 4시간봉
//    개수로 환산(하루 6봉)해 계산하므로 다른 시간봉에서는 검증과 다른 신호가 나옵니다.
//  · 검증 구간은 2017-12 ~ 2026-08, 전 구간 최적화 성적이며 **미래를 보장하지 않습니다.**
//    2024년 이후만 자르면 업비트 연 31~50%, 바이낸스 연 11~20%로 낮아집니다.
//  · 신호는 봉 마감 기준입니다. 알림은 "봉 마감 시 1회"로 설정하세요.
// ============================================================================
indicator("AlgoLab Verified 4 — BTC 4H", overlay=true, max_labels_count=500)

// ── 입력 ────────────────────────────────────────────────────────────────────
show1 = input.bool(true,  "① UT Bot 2.0/21 + SMA50",   group="세팅 표시")
show2 = input.bool(false, "② UT Bot 3.0/7 + SMA200",   group="세팅 표시")
show3 = input.bool(false, "③ 슈퍼트렌드 7/2.5 + SMA200", group="세팅 표시")
show4 = input.bool(false, "④ 켈트너 50/2.0 + SMA200",   group="세팅 표시")
showTbl = input.bool(true, "상태 표 표시", group="세팅 표시")
BPD = 6  // 4시간봉 기준 하루 봉수

// ── 공통 계산 ───────────────────────────────────────────────────────────────
// 일봉 추세필터: 백테스트와 동일하게 4시간봉 위에서 일수×6봉 SMA 로 계산한다
fOK(days) => close > ta.sma(close, days * BPD)

// UT Bot (QuantNomad 계열) — ATR 트레일링 스탑
utLong(key, atrN) =>
    loss = key * ta.atr(atrN)
    var float st = na
    prev = nz(st, close - loss)
    st := close > prev and close[1] > prev ? math.max(prev, close - loss) :
         close < prev and close[1] < prev ? math.min(prev, close + loss) :
         close > prev ? close - loss : close + loss
    close > st

// 슈퍼트렌드 — 밴드 추적 (백테스트 파이썬 구현과 동일 순서)
stLong(n, mult) =>
    a = ta.atr(n)
    upB = hl2 - mult * a
    dnB = hl2 + mult * a
    var float up = na
    var float dn = na
    var int   d  = 1
    prevUp = nz(up[1], upB)
    prevDn = nz(dn[1], dnB)
    up := close[1] > prevUp ? math.max(upB, prevUp) : upB
    dn := close[1] < prevDn ? math.min(dnB, prevDn) : dnB
    d  := close > prevDn ? 1 : close < prevUp ? -1 : nz(d[1], 1)
    d > 0

// 켈트너 돌파 — 상단 돌파 매수, 중심 EMA 이탈 청산 (상태 유지)
keltLong(n, k) =>
    e = ta.ema(close, n)
    a = ta.atr(n)
    var bool st = false
    st := close > e + k * a ? true : close < e ? false : st
    st

// ── 4개 세팅 상태 (신호 & 필터) ─────────────────────────────────────────────
s1 = utLong(2.0, 21) and fOK(50)
s2 = utLong(3.0, 7)  and fOK(200)
s3 = stLong(7, 2.5)  and fOK(200)
s4 = keltLong(50, 2.0) and fOK(200)

// ── 그리기 ──────────────────────────────────────────────────────────────────
plot(show1 ? ta.sma(close, 50 * BPD)  : na, "일봉 SMA50 환산",  color=color.new(color.teal, 55))
plot((show2 or show3 or show4) ? ta.sma(close, 200 * BPD) : na, "일봉 SMA200 환산", color=color.new(color.gray, 55))

on1 = show1 and s1 and not s1[1]
off1 = show1 and not s1 and s1[1]
plotshape(on1,  "① 진입", shape.triangleup,   location.belowbar, color.new(#00A862, 0), size=size.small, text="①IN")
plotshape(off1, "① 청산", shape.triangledown, location.abovebar, color.new(#E02B20, 0), size=size.small, text="①OUT")
plotshape(show2 and s2 and not s2[1], "② 진입", shape.triangleup,   location.belowbar, color.new(#00A862, 40), size=size.tiny, text="②")
plotshape(show2 and not s2 and s2[1], "② 청산", shape.triangledown, location.abovebar, color.new(#E02B20, 40), size=size.tiny, text="②")
plotshape(show3 and s3 and not s3[1], "③ 진입", shape.triangleup,   location.belowbar, color.new(#00A862, 40), size=size.tiny, text="③")
plotshape(show3 and not s3 and s3[1], "③ 청산", shape.triangledown, location.abovebar, color.new(#E02B20, 40), size=size.tiny, text="③")
plotshape(show4 and s4 and not s4[1], "④ 진입", shape.triangleup,   location.belowbar, color.new(#00A862, 40), size=size.tiny, text="④")
plotshape(show4 and not s4 and s4[1], "④ 청산", shape.triangledown, location.abovebar, color.new(#E02B20, 40), size=size.tiny, text="④")

nOn = (s1 ? 1 : 0) + (s2 ? 1 : 0) + (s3 ? 1 : 0) + (s4 ? 1 : 0)
bgcolor(nOn > 0 ? color.new(#00A862, 96 - nOn * 2) : na)

// ── 상태 표 ─────────────────────────────────────────────────────────────────
var table tb = table.new(position.top_right, 2, 5, bgcolor=color.new(color.white, 10), border_width=1)
if showTbl and barstate.islast
    table.cell(tb, 0, 0, "AlgoLab Verified 4", text_color=color.black, text_size=size.small)
    table.cell(tb, 1, 0, nOn > 0 ? str.tostring(nOn) + "/4 ON" : "0/4", text_color=nOn > 0 ? #00A862 : color.gray, text_size=size.small)
    table.cell(tb, 0, 1, "① UT Bot 2.0/21+50",  text_color=color.black, text_size=size.tiny)
    table.cell(tb, 1, 1, s1 ? "보유" : "현금", text_color=s1 ? #00A862 : color.gray, text_size=size.tiny)
    table.cell(tb, 0, 2, "② UT Bot 3.0/7+200",  text_color=color.black, text_size=size.tiny)
    table.cell(tb, 1, 2, s2 ? "보유" : "현금", text_color=s2 ? #00A862 : color.gray, text_size=size.tiny)
    table.cell(tb, 0, 3, "③ 슈퍼트렌드 7/2.5+200", text_color=color.black, text_size=size.tiny)
    table.cell(tb, 1, 3, s3 ? "보유" : "현금", text_color=s3 ? #00A862 : color.gray, text_size=size.tiny)
    table.cell(tb, 0, 4, "④ 켈트너 50/2.0+200",  text_color=color.black, text_size=size.tiny)
    table.cell(tb, 1, 4, s4 ? "보유" : "현금", text_color=s4 ? #00A862 : color.gray, text_size=size.tiny)

// ── 알림 ────────────────────────────────────────────────────────────────────
alertcondition(s1 and not s1[1], "① UT Bot 2.0/21+SMA50 진입", "AlgoLab ①: 진입 신호(봉 마감 확인)")
alertcondition(not s1 and s1[1], "① UT Bot 2.0/21+SMA50 청산", "AlgoLab ①: 청산 신호(봉 마감 확인)")
alertcondition(s2 and not s2[1], "② UT Bot 3.0/7+SMA200 진입", "AlgoLab ②: 진입 신호")
alertcondition(not s2 and s2[1], "② UT Bot 3.0/7+SMA200 청산", "AlgoLab ②: 청산 신호")
alertcondition(s3 and not s3[1], "③ 슈퍼트렌드 진입", "AlgoLab ③: 진입 신호")
alertcondition(not s3 and s3[1], "③ 슈퍼트렌드 청산", "AlgoLab ③: 청산 신호")
alertcondition(s4 and not s4[1], "④ 켈트너 진입", "AlgoLab ④: 진입 신호")
alertcondition(not s4 and s4[1], "④ 켈트너 청산", "AlgoLab ④: 청산 신호")

재현 코드 — 1,776번 검증을 그대로

파이썬 하나로 데이터 수집부터 전체 스윕까지. 기본 실행은 생존 4개 재현(몇 분), --full 은 888조합 × 2거래소 전체(수십 분)입니다.

pip install numpy 만 있으면 됩니다.

python tv_indicator_backtest.py — 생존 4개 + 착시 사례 + 보유 재현.

python tv_indicator_backtest.py --full — 1,776번 전체.

tv_indicator_backtest.py
파일 받기
# -*- coding: utf-8 -*-
"""트레이딩뷰 인기 지표 888조합 × 두 거래소 검증 — 영상 재현 코드 (AlgoLab)

영상: 「트레이딩뷰 지표 888개, 1000만원이 6억과 5만원」
- 기본 실행: 생존 4개 조합 + 슈퍼트렌드 착시 사례 + 그냥 보유를 재현합니다 (몇 분).
- 전체 재현: `python tv_indicator_backtest.py --full` — 13종 888조합 × 업비트·바이낸스
  = 1776번 백테스트 전부 (수십 분 걸립니다).

데이터: 업비트 원화(KRW-BTC/ETH)·바이낸스 달러(BTCUSDT/ETHUSDT) 4시간봉, 공개 API.
처음 실행하면 내려받아 CSV 로 저장하고, 다음부터는 재사용합니다.

⚠ ASOF: 영상 제작 시점(2026-08-31)까지로 데이터를 잘라야 영상과 같은 숫자가 나옵니다.
   오늘까지 보려면 ASOF = None 으로 바꾸세요(숫자는 영상과 달라집니다).
⚠ 이 코드는 시뮬레이션입니다. 4시간봉 종가 체결·편도 0.1% 비용을 가정했고 슬리피지는
   반영하지 않았습니다. 과거 성적은 미래 수익을 보장하지 않습니다.
"""
import csv, datetime, json, os, sys, time, urllib.parse, urllib.request
import numpy as np
from numpy.lib.stride_tricks import sliding_window_view as swv

ASOF = "2026-08-31T12:00:00"  # 영상 제작 기준 마지막 봉(UTC). None 이면 오늘까지.
# ⚠ 날짜가 아니라 봉 시각까지 고정한 이유: 같은 날이라도 뒤 봉 2개가 더 들어오면
#   바이낸스 쪽 수치가 0.1%p 단위로 밀린다(실측). 영상과 자릿수까지 같으려면 이 시각 그대로.
# ⚠ 그래도 남는 오차: 영상 데이터는 마지막 봉이 확정되기 전에 수집됐습니다. 그 한 봉 차이로
#   바이낸스 검증 구간 일부 수치가 ±0.3%p 안에서 다를 수 있습니다(예: 20.3% ↔ 20.6%).
#   본편에 나온 배수·낙폭 등 핵심 수치는 전부 자릿수까지 일치함을 확인했습니다(2026-09-02).
COST = 0.001                 # 편도 0.1%
WARM = 400
VAL_START = "2024-01-01"     # 검증 구간 시작
HERE = os.path.dirname(os.path.abspath(__file__))


# ── 데이터 수집 (공개 API · 캐시) ────────────────────────────────────────────
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, out):
    if os.path.exists(out):
        return
    print(f"  업비트 {market} 4시간봉 내려받는 중 …")
    rows, to = [], ""
    while True:
        url = (f"https://api.upbit.com/v1/candles/minutes/240?market={market}&count=200"
               + (f"&to={urllib.parse.quote(to)}" if to else ""))
        try:
            batch = _get(url)
        except Exception as e:
            print(f"   재시도({e})"); time.sleep(2); continue
        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()
    with open(out, "w", newline="", encoding="utf-8") as f:
        w = csv.writer(f); w.writerow(["date", "open", "high", "low", "close"]); w.writerows(rows)
    print(f"  ✓ {market} {len(rows):,}봉 → {os.path.basename(out)}")


def fetch_binance(symbol, out):
    if os.path.exists(out):
        return
    print(f"  바이낸스 {symbol} 4시간봉 내려받는 중 …")
    rows, start = [], int(datetime.datetime(2017, 8, 1).timestamp() * 1000)
    while True:
        url = (f"https://api.binance.com/api/v3/klines?symbol={symbol}&interval=4h"
               f"&limit=1000&startTime={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)
    with open(out, "w", newline="", encoding="utf-8") as f:
        w = csv.writer(f); w.writerow(["date", "open", "high", "low", "close"]); w.writerows(rows)
    print(f"  ✓ {symbol} {len(rows):,}봉 → {os.path.basename(out)}")


def load(path):
    """CSV → 배열. ASOF 자르기는 캐시든 새로 받든 똑같이 여기서 적용한다."""
    d, o, h, l, c = [], [], [], [], []
    for r in csv.DictReader(open(path, encoding="utf-8")):
        if ASOF and r["date"] > ASOF:
            continue
        d.append(r["date"]); o.append(float(r["open"])); h.append(float(r["high"]))
        l.append(float(r["low"])); c.append(float(r["close"]))
    return d, np.array(o), np.array(h), np.array(l), np.array(c)


# ── 기초 유틸 (영상 백테스트와 동일 구현) ───────────────────────────────────
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 _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 _wma(x, n):
    out = np.full(len(x), np.nan)
    if len(x) >= n:
        w = np.arange(1, n + 1, dtype=float)
        out[n-1:] = swv(x, n) @ w / w.sum()
    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()
        k = 1.0 / n
        for i in range(n, len(tr)):
            a[i] = a[i-1] + k * (tr[i] - a[i-1])
    return a


def _rstd(x, n):
    out = np.full(len(x), np.nan)
    if len(x) >= n:
        out[n-1:] = swv(x, n).std(axis=1)
    return out


def _ffill_signal(buy, sell, n_warm, n):
    s = np.full(n, np.nan)
    s[sell] = 0.0
    s[buy] = 1.0
    s[:n_warm] = 0.0
    idx = np.arange(n)
    has = ~np.isnan(s)
    last = np.maximum.accumulate(np.where(has, idx, 0))
    out = s[last]
    out[np.isnan(out)] = 0.0
    return out


# ── 전략 13종 (트레이딩뷰 인기 + 고전) ──────────────────────────────────────
def absmom(o, h, l, c, look=252):
    s = np.zeros(len(c))
    s[look:] = (c[look:] / c[:-look] > 1).astype(float)
    return s


def ma_cross(o, h, l, c, fast=50, 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


def donchian(o, h, l, c, n=55):
    hh = np.full(len(c), np.nan); ll = np.full(len(c), np.nan)
    if len(c) > n:
        hh[n:] = swv(h, n).max(axis=1)[:-1]
        ll[n:] = swv(l, n).min(axis=1)[:-1]
    return _ffill_signal(c > hh, c < ll, n + 1, len(c))


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 ut_bot(o, h, l, c, key=2.0, atr_n=10):
    a = _atr(h, l, c, atr_n)
    pos = np.zeros(len(c))
    st = np.nan
    for i in range(len(c)):
        if np.isnan(a[i]):
            continue
        loss = key * a[i]
        if np.isnan(st):
            st = c[i] - loss
        prev = st
        if c[i] > prev and c[i-1] > prev:
            st = max(prev, c[i] - loss)
        elif c[i] < prev and c[i-1] < prev:
            st = min(prev, c[i] + loss)
        else:
            st = c[i] - loss if c[i] > prev else c[i] + loss
        pos[i] = 1.0 if c[i] > st else 0.0
    return pos


def hull_trend(o, h, l, c, n=60):
    half = max(2, n // 2); rt = max(2, int(np.sqrt(n)))
    raw = 2 * _wma(c, half) - _wma(c, n)
    hma = np.full(len(c), np.nan)
    ok = ~np.isnan(raw)
    if ok.sum() >= rt:
        i0 = np.argmax(ok)
        hma[i0+rt-1:] = _wma(raw[i0:], rt)[rt-1:]
    out = np.zeros(len(c))
    m = np.zeros(len(c), dtype=bool)
    m[1:] = ~(np.isnan(hma[1:]) | np.isnan(hma[:-1]))
    out[m] = (hma[1:][m[1:]] > hma[:-1][m[1:]]).astype(float)
    return out


def keltner_break(o, h, l, c, n=20, k=1.5):
    e = _ema(c, n); a = _atr(h, l, c, n)
    return _ffill_signal((c > e + k * a) & ~np.isnan(a), c < e, n + 1, len(c))


def ichimoku_trend(o, h, l, c, conv=9, base=26):
    cv = np.full(len(c), np.nan); bs = np.full(len(c), np.nan)
    if len(c) > base:
        cv[conv-1:] = (swv(h, conv).max(axis=1) + swv(l, conv).min(axis=1)) / 2
        bs[base-1:] = (swv(h, base).max(axis=1) + swv(l, base).min(axis=1)) / 2
    out = np.zeros(len(c))
    m = ~(np.isnan(cv) | np.isnan(bs))
    out[m] = ((c[m] > bs[m]) & (cv[m] > bs[m])).astype(float)
    return out


def macd_trend(o, h, l, c, fast=12, slow=26, sig=9):
    m = _ema(c, fast) - _ema(c, slow)
    return (m > _ema(m, sig)).astype(float)


def rsi_meanrev(o, h, l, c, n=14, lo=30, hi=70):
    d = np.diff(c, prepend=c[0])
    up = np.where(d > 0, d, 0.0); dn = np.where(d < 0, -d, 0.0)
    au = _ema(up, n); ad = _ema(dn, n)
    rsi = 100 - 100 / (1 + au / np.where(ad == 0, 1e-9, ad))
    return _ffill_signal(rsi < lo, rsi > hi, n + 1, len(c))


def bollinger_meanrev(o, h, l, c, n=20, k=2.0):
    ma = _sma(c, n); sd = _rstd(c, n)
    return _ffill_signal((c < ma - k * sd) & ~np.isnan(sd), c > ma, n + 1, len(c))


def wavetrend(o, h, l, c, n1=10, n2=21):
    ap = (h + l + c) / 3
    esa = _ema(ap, n1)
    dd = _ema(np.abs(ap - esa), n1)
    ci = (ap - esa) / np.where(dd == 0, 1e-9, 0.015 * dd)
    wt1 = _ema(ci, n2)
    wt2 = _sma(wt1, 4)
    out = np.zeros(len(c))
    m = ~np.isnan(wt2); out[m] = (wt1[m] > wt2[m]).astype(float)
    out[:n1+n2] = 0.0
    return out


def squeeze_mom(o, h, l, c, n=20):
    hh = np.full(len(c), np.nan); ll = np.full(len(c), np.nan)
    if len(c) >= n:
        hh[n-1:] = swv(h, n).max(axis=1)
        ll[n-1:] = swv(l, n).min(axis=1)
    mid = ((hh + ll) / 2 + _sma(c, n)) / 2
    src = c - mid
    mom = np.full(len(c), np.nan)
    if len(c) >= 2 * n:
        x = np.arange(n, dtype=float)
        xm = x - x.mean()
        den = (xm ** 2).sum()
        win = swv(np.nan_to_num(src), n)
        beta = win @ xm / den
        alpha = win.mean(axis=1)
        mom[n-1:] = alpha + beta * xm[-1]
        mom[:2*n] = np.nan
    out = np.zeros(len(c))
    m = ~np.isnan(mom); out[m] = (mom[m] > 0).astype(float)
    return out


POOL = {
    "절대 모멘텀":    (absmom, [{"look": l} for l in (63, 126, 189, 252)]),
    "이동평균 크로스": (ma_cross, [{"fast": f, "slow": s} for f in (10, 20, 50)
                              for s in (60, 100, 150, 200) if f < s]),
    "돈치안 돌파":    (donchian, [{"n": n} for n in (10, 20, 40, 55, 100)]),
    "슈퍼트렌드":     (supertrend, [{"n": n, "mult": m} for n in (7, 10, 14, 20)
                               for m in (1.5, 2.0, 2.5, 3.0, 3.5, 4.0)]),
    "UT Bot":       (ut_bot, [{"key": k, "atr_n": n} for k in (1.0, 2.0, 3.0)
                              for n in (7, 10, 14, 21)]),
    "헐MA 추세":     (hull_trend, [{"n": n} for n in (20, 40, 60, 100, 150)]),
    "켈트너 돌파":    (keltner_break, [{"n": n, "k": k} for n in (20, 50)
                                 for k in (0.5, 1.0, 1.5, 2.0)]),
    "일목 추세":     (ichimoku_trend, [{"conv": cv, "base": b}
                                    for cv, b in ((9, 26), (7, 22), (12, 33))]),
    "MACD 추세":    (macd_trend, [{"fast": f, "slow": s, "sig": g}
                                for f, s, g in ((8, 21, 5), (12, 26, 9), (16, 32, 9), (20, 50, 10))]),
    "RSI 평균회귀":  (rsi_meanrev, [{"n": n, "lo": lo, "hi": hi} for n in (2, 7, 14)
                                for lo in (20, 30) for hi in (60, 70, 80)]),
    "볼린저 평균회귀": (bollinger_meanrev, [{"n": n, "k": k} for n in (20, 30)
                                     for k in (1.5, 2.0, 2.5)]),
    "WaveTrend":    (wavetrend, [{"n1": a, "n2": b} for a in (8, 10, 13) for b in (15, 21)]),
    "Squeeze 모멘텀": (squeeze_mom, [{"n": n} for n in (14, 20, 30, 40)]),
}
FILTS = (0, 50, 100, 200)    # 일봉 추세필터: 없음 / SMA50 / 100 / 200 (4h봉 × 6 = 하루)


# ── 러너 ────────────────────────────────────────────────────────────────────
def series(path):
    d, o, h, l, c = load(path)
    n = len(c)
    ret = np.zeros(n); ret[1:] = c[1:] / c[:-1] - 1
    yrs = (datetime.date.fromisoformat(d[-1][:10])
           - datetime.date.fromisoformat(d[0][:10])).days / 365.25
    return dict(d=d, o=o, h=h, l=l, c=c, n=n, ret=ret, ppy=n / yrs,
                val_i=int(np.searchsorted(np.array(d), VAL_START)))


def run(S, fn, param, filt, cost=COST):
    sig = fn(S["o"], S["h"], S["l"], S["c"], **param)
    if filt:
        sma = _sma(S["c"], filt * 6)
        f = np.zeros(S["n"]); m = ~np.isnan(sma)
        f[m] = (S["c"][m] > sma[m]).astype(float)
        sig = sig * f
    pos = np.r_[0.0, sig[:-1]]
    g = (1 + pos * S["ret"]) * (1 - cost * np.abs(np.diff(np.r_[0.0, pos])))
    return g


def stats(g, ppy):
    eq = np.cumprod(g)
    yrs = len(g) / ppy
    cagr = eq[-1] ** (1 / yrs) - 1 if eq[-1] > 0 else -1.0
    mdd = float((eq / np.maximum.accumulate(eq) - 1).min())
    return float(cagr), mdd, float(eq[-1])


def cell(Su, Sb, fn, param, filt):
    """한 조합을 업비트+바이낸스에서 돌려 (업, 바, min CAGR, 최악 MDD, 통과) 반환."""
    out = []
    for S in (Su, Sb):
        g = run(S, fn, param, filt)
        cagr, mdd, mult = stats(g[WARM:], S["ppy"])
        vc, vm, _ = stats(g[S["val_i"]:], S["ppy"])
        out.append(dict(cagr=cagr, mdd=mdd, mult=mult, val_cagr=vc, val_mdd=vm))
    mn = min(out[0]["cagr"], out[1]["cagr"])
    wm = min(out[0]["mdd"], out[1]["mdd"])
    return out[0], out[1], mn, wm, (mn >= 0.30 and wm >= -0.35)


def pct(x):
    return f"{x*100:.1f}%"


def main():
    full = "--full" in sys.argv
    print(f"기준일 ASOF = {ASOF or '오늘'} · 비용 편도 {COST*100:.1f}% · 4시간봉\n")
    fetch_upbit("KRW-BTC", os.path.join(HERE, "upbit_btc_4h.csv"))
    fetch_upbit("KRW-ETH", os.path.join(HERE, "upbit_eth_4h.csv"))
    fetch_binance("BTCUSDT", os.path.join(HERE, "binance_btc_4h.csv"))
    fetch_binance("ETHUSDT", os.path.join(HERE, "binance_eth_4h.csv"))
    Su = series(os.path.join(HERE, "upbit_btc_4h.csv"))
    Sb = series(os.path.join(HERE, "binance_btc_4h.csv"))
    Eu = series(os.path.join(HERE, "upbit_eth_4h.csv"))
    Eb = series(os.path.join(HERE, "binance_eth_4h.csv"))
    print(f"업비트 BTC {Su['d'][0][:10]} ~ {Su['d'][-1][:10]} ({Su['n']:,}봉)\n")

    print("=" * 74)
    print("① 잣대 통과 4개 조합 (영상의 생존자)")
    print("   잣대: 두 거래소 모두 연복리 30% 이상 · 최대낙폭 -35% 이내")
    print("=" * 74)
    SURV = [
        ("UT Bot",  ut_bot,        {"key": 2.0, "atr_n": 21}, 50),
        ("UT Bot",  ut_bot,        {"key": 3.0, "atr_n": 7},  200),
        ("슈퍼트렌드", supertrend,    {"n": 7, "mult": 2.5},     200),
        ("켈트너",   keltner_break, {"n": 50, "k": 2.0},       200),
    ]
    for name, fn, param, filt in SURV:
        u, b, mn, wm, ok = cell(Su, Sb, fn, param, filt)
        print(f"\n{name} {param} + 일봉SMA{filt}")
        print(f"  업비트 : 연복리 {pct(u['cagr'])} · 최대낙폭 {pct(u['mdd'])} · {u['mult']:.1f}배"
              f" · 검증(2024~) {pct(u['val_cagr'])}")
        print(f"  바이낸스: 연복리 {pct(b['cagr'])} · 최대낙폭 {pct(b['mdd'])} · {b['mult']:.1f}배"
              f" · 검증(2024~) {pct(b['val_cagr'])}")
        print(f"  → min 연복리 {pct(mn)} · 잣대 {'통과' if ok else '탈락'}")

    print("\n" + "=" * 74)
    print("② 단일 거래소 착시 (영상의 슈퍼트렌드 사례 — 업비트만 보면 통과처럼 보인다)")
    print("=" * 74)
    u, b, mn, wm, ok = cell(Su, Sb, supertrend, {"n": 20, "mult": 3.5}, 100)
    print(f"슈퍼트렌드 n20/mult3.5 + SMA100")
    print(f"  업비트 : {u['mult']:.1f}배 ({pct(u['cagr'])}/{pct(u['mdd'])})")
    print(f"  바이낸스: {b['mult']:.1f}배 ({pct(b['cagr'])}/{pct(b['mdd'])}) ← 같은 조합")

    print("\n" + "=" * 74)
    print("③ 그냥 보유 (업비트 비트코인, 같은 구간)")
    print("=" * 74)
    hc, hm, hx = stats(1 + Su["ret"][WARM:], Su["ppy"])
    print(f"  연복리 {pct(hc)} · 최대낙폭 {pct(hm)} · {hx:.1f}배")

    if not full:
        print("\n(전체 888조합 × 2거래소 = 1776번 재현: --full 옵션 · 수십 분)")
        return

    print("\n" + "=" * 74)
    print("④ 전체 스윕 — 13종 888조합 × 업비트·바이낸스 (BTC와 ETH 각각)")
    print("=" * 74)
    n_all = n_pass = n_ret = 0
    passed = []
    for strat, (fn, params) in POOL.items():
        for asset, (Au, Ab) in (("BTC", (Su, Sb)), ("ETH", (Eu, Eb))):
            for param in params:
                for filt in FILTS:
                    u, b, mn, wm, ok = cell(Au, Ab, fn, param, filt)
                    n_all += 1
                    n_ret += mn >= 0.30
                    if ok:
                        n_pass += 1
                        passed.append((strat, asset, param, filt, mn, u, b))
        print(f"  {strat} 완료")
    print(f"\n총 {n_all}칸 · 수익률 관문(min 30%) 통과 {n_ret}칸 · 두 관문 모두 통과 {n_pass}칸")
    for strat, asset, param, filt, mn, u, b in sorted(passed, key=lambda x: -x[4]):
        print(f"  {strat} {asset} {param} SMA{filt} — min {pct(mn)}"
              f" | 업 {u['mult']:.1f}배/{pct(u['mdd'])} | 바 {b['mult']:.1f}배/{pct(b['mdd'])}")


if __name__ == "__main__":
    main()

이 규칙, 사람이 9년 지킬 수 있을까요

매매는 연 12~33번, 4시간봉 캔들 마감은 새벽에도 옵니다. 2018·2022년에는 1년 내내 신호 없이 기다려야 했고, 한 번이라도 끼어들면 '잃지 않은 해'의 방어가 사라집니다. 그래서 저희는 검증된 규칙을 봇에게 맡깁니다.

직접 하실 분

  • 위 지표 파일과 재현 코드를 그대로 가져가세요
  • 설정값·규칙 전부 이 페이지에 공개되어 있습니다
  • 질문은 유튜브 댓글로 — 다음 검증 편에 반영합니다

맡기실 분

Strategy Lab · 이 검증 더 보기
이 검증을 영상으로 보기4분 49초 신상 스크립트 448조합 — 인기 1등 탈락같은 잣대 전략 후보 1,887개 전수 백테스트대량 검증 전략 검증 전체 보기 →

※ 본 페이지의 수치는 공개된 시장 데이터를 과거 기준으로 검증한 교육·검증 목적의 자료이며, 특정 종목·전략의 매수·매도 권유가 아닙니다. 과거 백테스트 성과는 미래 수익을 보장하지 않습니다. 지표 파일과 코드는 무료이며, 사용에 따른 모든 투자 판단과 책임은 이용자 본인에게 있습니다.