RSI SETTINGS — 2019-09-01 ~ 2026-09-06 · 비트코인 4시간봉

RSI 설정값 360칸이 전부 탈락했습니다.
살아남은 건 설정값이 아니라 넣는 자리였습니다

「RSI 14 / 30 / 70」 처럼 RSI 자체를 전략으로 쓰는 설정을 10코인 두 시장에 전수로 걸면(설정 18종 × 일봉 필터 둘 × 10코인 = 360칸) 우리 잣대(두 시장 연 30% 이상 · 깊은 쪽 최대낙폭 −35% 이내)를 넘는 칸이 0칸입니다. 대신 이미 돌아가는 추세 봇의 진입 자리에 RSI 한 줄을 얹으면 이야기가 달라집니다. 이 페이지는 그 한 줄과, 그게 무엇을 못 하는지까지 전부 공개합니다.

받아갈 한 줄
4시간봉 · 봇은 슈퍼트렌드 (10, 3) · 일봉 종가가 30일 이동평균 위일 때만
진입 조건에 ta.rsi(close, 14) > 78 를 AND 로 더합니다 — 신호가 켜져도 바로 사지 않고 이 조건이 참이 되는 첫 봉에 삽니다. 청산은 그대로(슈퍼트렌드가 꺼지는 봉)
길이 14는 트레이딩뷰 RSI 기본값입니다. 지표 설정을 건드릴 필요가 없습니다.
RSI 를 그대로 전략으로 (360칸)두 시장 min 연복리 중앙잣대 통과
일봉 필터 없이-38.2%0칸
일봉 30일선 필터를 켜고-10.5%0칸
가장 나은 설정 — RSI14 lo20 hi60 (필터 켬)-3.5%
가장 나쁜 설정 — RSI2 lo30 hi60 (필터 끔)-54.4%
그냥 보유14.7%
가장 많이 말하는 기본값 RSI 14 / 30 / 70 도 20칸 중 통과 0 입니다. 일봉 필터 한 줄이 성적을 통째로 들어올리지만(-38.2% → -10.5%) 그래도 잣대는 못 넘습니다. RSI 는 전략이 아닙니다.
재현 코드 받기 이 설정이 못 하는 것 →

1. 잣대를 못 넘던 봇이 넘습니다

바이낸스 BTCUSDT · 업비트 KRW-BTC · 4시간봉 · 편도 0.1% · 다음 봉 체결 · 워밍업 400봉 · 성적 구간 2019-11-06~2026-09-06(6.83년) · 헤드라인은 두 시장 중 나쁜 쪽입니다.

두 시장 min 연복리 / worst 최대낙폭연복리최대낙폭잣대
봇 (RSI 없음)32.9% -36.7%탈락
봇 + 한 줄33.6% -22.5%통과
낙폭이 -36.7% → -22.5% 로 줄어드는 동안 연복리는 32.9% → 33.6% 로 거의 그대로입니다. Calmar 로는 0.90 → 1.49(바이낸스) · 1.86 → 2.37(업비트). 검증 구간(2024-01-01~)도 두 시장 다 개선됩니다 — 16.4%→23.3% · 32.6%→32.0%, 낙폭 -28.1%→-12.7% · -20.8%→-12.1%.

바이낸스 BTCUSDT

바이낸스 BTCUSDT (1,000만원 시작)연복리최대낙폭노출 Calmar거래최종 잔고
그냥 보유36.9%-77.0%1.000.481회8,547만원
봇 — 슈퍼트렌드 + 일봉 30일선32.9%-36.7%0.380.90201회6,986만원
봇 + 한 줄33.6%-22.5%0.161.4945회7,218만원

업비트 KRW-BTC

업비트 KRW-BTC (1,000만원 시작)연복리최대낙폭노출 Calmar거래최종 잔고
그냥 보유40.3%-74.8%1.000.541회1억 114만원
봇 — 슈퍼트렌드 + 일봉 30일선58.1%-31.2%0.381.86181회2억 2,824만원
봇 + 한 줄46.4%-19.6%0.172.3746회1억 3,544만원
업비트에서는 돈이 줍니다. 2억 2,824만원 → 1억 3,544만원. 대신 낙폭이 -31.2% → -19.6% 로 얕아집니다. 이 한 줄은 돈을 버는 장치가 아니라 낙폭을 사는 장치입니다.

2. 메커니즘 — 덜 사는 게 아니라 늦게 삽니다

거래 201회 → 45회(업비트 181 → 46회) · 노출 0.38 → 0.16 · 승률 37.8% → 51.1%

승률은 메커니즘 설명이지 성적이 아닙니다 — 승률이 높아도 손익비가 나쁘면 집니다. 이 표에서 봐야 할 것은 노출입니다. 봇이 시장 안에 있던 시간이 3분의 1 아래로 줄었습니다.

3. 반론 — "낙폭이 얕은 건 그냥 덜 넣어서 아닌가"

낙폭 — '낙폭이 얕아진 건 시장에 덜 있어서 아니냐'
낙폭으로 물었으니 낙폭으로 답합니다. 같은 평균 노출을 만드는 계좌를 나란히 세웠습니다.

바이낸스 · 같은 평균 노출 0.16최대낙폭연복리1,000만원 →
봇 + 한 줄-22.5%33.6%7,218만원
그냥 34봉 늦게 사기-23.3%20.5%3,574만원
같은 참-비율 무작위 조건 30회(중앙)-32.3%20.1%
비중만 줄여 늘 들고 있기 (w=0.42)-17.0%14.2%2,481만원
업비트 · 같은 평균 노출 0.17최대낙폭연복리1,000만원 →
봇 + 한 줄-19.6%46.4%1억 3,544만원
그냥 33봉 늦게 사기-23.2%28.2%5,472만원
같은 참-비율 무작위 조건 30회(중앙)-25.7%32.4%
비중만 줄여 늘 들고 있기 (w=0.44)-14.5%23.3%4,189만원
늦게 산 것도, 아무 때나 덜 산 것도 아닙니다. 같은 노출로 그냥 지연시킨 계좌와 무작위로 비운 계좌는 낙폭이 더 깊습니다. 무작위 조건 30회 분포에서 이 칸은 낙폭 백분위 90%(업비트 93%)에 섭니다.
다만 낙폭 하나만 놓고 겨루면 집니다. 비중만 줄여 늘 들고 있는 계좌가 -17.0% 로 더 얕습니다 (업비트 -14.5%). 그러니 "낙폭이 제일 얕다"고 말하지 않습니다 — 이 줄이 하는 일은 그 낙폭대에서 돈을 안 깎는 것입니다(7,218만원 대 2,481만원).

4. 이 설정이 못 하는 것

10코인 × 2시장 × 전략 4종 · 같은 한 줄을 얹었을 때 · 72/80 칸에서 낙폭이 얕아졌지만 수익이 는 칸은 28/80 입니다.

전략 (20칸씩)연복리 변화 중앙최대낙폭 변화 중앙낙폭이 얕아진 칸
슈퍼트렌드-0.6%p+12.1%p18/20
돈치안+3.9%p+11.4%p18/20
UT봇-12.5%p+12.7%p19/20
켈트너-10.6%p+10.9%p17/20
비용을 올리면 (두 시장 min 연복리)연복리잣대
편도 0.1%33.6%통과
편도 0.2%31.8%통과
편도 0.3%30.1%통과
편도 0.5%26.7%탈락
일봉 30일선 필터를 꺼도 통과합니다(32.0% / -24.8%). 필터는 이 한 줄의 전제가 아닙니다.

5. 재현 코드 — 숫자를 전부 이 파일이 냅니다

시세를 함께 얼려 두었습니다. 인터넷 없이 python run_rsi.py 로 위 표가 그대로 나옵니다.

run_rsi.py 다운로드
# -*- coding: utf-8 -*-
"""알고랩 — 「비트코인 봇, RSI는 이 자리에」 편 재현 코드 (2026-09-16 공개)

영상과 페이지에 나오는 숫자를 **전부** 이 파일 하나가 냅니다.
시세는 data_rsi/ 에 얼려 두었으니 인터넷 없이 그대로 돌아갑니다.

    python run_rsi.py            (numpy 만 있으면 됩니다)

봇   : 비트코인 4시간봉 · 슈퍼트렌드(10, 3.0) · 일봉 종가가 30일 이동평균 위일 때만
한 줄: 신호가 켜져도 바로 사지 않고, ta.rsi(close, 14) > 78 이 참이 되는 **첫 봉**에 산다
       (트레이딩뷰의 ta.rsi 는 Wilder 평활(RMA)입니다 — EMA 로 짜면 다른 지표가 됩니다)
시장 : 바이낸스 BTCUSDT + 업비트 KRW-BTC, 헤드라인은 항상 **두 시장 중 나쁜 쪽**
정직 : 신호 다음 봉 체결(룩어헤드 없음) · 편도 0.1% · 워밍업 400봉은 성적에서 뺀다
구간 : 2019-09-01 ~ 2026-09-06 (성적 구간 2019-11-06~, 6.83년) · 검증 구간 2024-01-01~

주의 : 이 한 줄은 **수익을 늘리는 장치가 아닙니다.** 10코인 x 2시장 x 전략 4종 80칸에서
       연복리 중앙값은 4.2%p 내려가고 최대낙폭 중앙값이 11.8%p 얕아집니다(얕아진 칸 72/80).
"""
import datetime
import os
import sys

import numpy as np

sys.stdout.reconfigure(encoding="utf-8", errors="replace")
HERE = os.path.dirname(os.path.abspath(__file__))
FEE, WARM, BPD, VAL = 0.001, 400, 6, "2024-01-01"     # 4시간봉 = 하루 6봉
ST_N, ST_MULT, FILT_DAYS = 10, 3.0, 30
RSI_N, RSI_T = 14, 78
SEED = 1000.0                                          # 만원
FILES = {"바이낸스": os.path.join(HERE, "data_rsi", "BTCUSDT_4h.csv"),
         "업비트":   os.path.join(HERE, "data_rsi", "KRW-BTC_4h.csv")}


# ── 지표 수학 ────────────────────────────────────────────────────────
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 rma(x, n):
    """Wilder 평활 = 트레이딩뷰 ta.rma. 첫 값은 SMA(n) 시드."""
    out = np.full(len(x), np.nan)
    if len(x) < n:
        return out
    out[n - 1] = np.mean(x[:n])
    a = 1.0 / n
    for i in range(n, len(x)):
        out[i] = a * x[i] + (1 - a) * out[i - 1]
    out[:n - 1] = out[n - 1]
    return out


def rsi_tv(c, n):
    """트레이딩뷰 ta.rsi(close, n)."""
    d = np.diff(c, prepend=c[0]); d[0] = 0.0
    up = np.where(d > 0, d, 0.0); dn = np.where(d < 0, -d, 0.0)
    au, ad = rma(up, n), rma(dn, n)
    return 100 - 100 / (1 + au / np.where(ad == 0, 1e-9, ad))


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 supertrend(h, l, c, n=ST_N, mult=ST_MULT):
    """슈퍼트렌드 — 위로 서 있으면 1, 아니면 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 apply_wait(sig, cond):
    """신호가 켜져 있는 동안 조건이 **처음** 참이 되는 봉에 진입한다.

    Pine 으로 옮길 때 별도 상태 변수가 필요 없습니다 —
    무포지션 and baseLong and rsi > 78 이면 strategy.entry,
    baseLong 이 꺼지는 봉에 strategy.close 면 같은 숫자가 나옵니다.
    """
    out = np.zeros(len(sig))
    holding = False
    for t in range(len(sig)):
        if sig[t] <= 0:
            holding = False
            continue
        if not holding and cond[t]:
            holding = True
        if holding:
            out[t] = sig[t]
    return out


# ── 계좌 ────────────────────────────────────────────────────────────
def load(path):
    d, o, h, l, c = [], [], [], [], []
    with open(path, encoding="utf-8") as f:
        next(f)
        for line in f:
            p = line.rstrip("\n").split(",")
            if len(p) < 5:
                continue
            d.append(p[0]); o.append(float(p[1])); h.append(float(p[2]))
            l.append(float(p[3])); c.append(float(p[4]))
    d = np.asarray(d)
    o, h, l, c = (np.asarray(x) for x in (o, h, l, c))
    ret = np.zeros(len(c)); 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, ret=ret, n=len(c), ppy=len(c) / yrs,
                val_i=int(np.searchsorted(d, VAL)))


def run(S, sig, fee=FEE):
    """신호는 **다음 봉**에 반영된다(룩어헤드 없음). 비용은 비중이 바뀐 만큼만."""
    pos = np.r_[0.0, sig[:-1]]
    g = (1 + pos * S["ret"]) * (1 - fee * np.abs(np.diff(np.r_[0.0, pos])))
    return pos, g


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


def won(man):
    man = round(man)
    if man >= 10000:
        e, m = divmod(man, 10000)
        return "%d억 %s만원" % (e, format(m, ",")) if m else "%d억원" % e
    return "%s만원" % format(man, ",")


def report(S, sig, label, fee=FEE):
    pos, g = run(S, sig, fee)
    cagr, mdd, mult = span(g[WARM:], S["ppy"])
    vcagr, vmdd, _ = span(g[max(WARM, S["val_i"]):], S["ppy"])
    on = pos > 0
    starts = np.where(on & ~np.r_[False, on[:-1]])[0]
    ends = np.where(~on & np.r_[False, on[:-1]])[0]
    if len(ends) < len(starts):
        ends = np.r_[ends, S["n"] - 1]
    r = np.array([S["c"][min(b, S["n"] - 1)] / S["c"][a] - 1 for a, b in zip(starts, ends)])
    print("  %-26s 연 %6.1f%% · 최대낙폭 %6.1f%% · Calmar %5.2f · 노출 %.2f · "
          "거래 %3d회 · 승률 %4.1f%% · 검증 %5.1f%%/%6.1f%% · %.0f만원 → %s"
          % (label, cagr * 100, mdd * 100, cagr / abs(mdd) if mdd < 0 else float("nan"),
             pos[WARM:].mean(), len(starts),
             (r > 0).mean() * 100 if len(r) else float("nan"),
             vcagr * 100, vmdd * 100, SEED, won(mult * SEED)))
    return cagr, mdd


def daily_filter(S):
    """일봉 종가가 30일 이동평균 위일 때만 1. 4시간봉이므로 30일 = 180봉."""
    m = sma(S["c"], FILT_DAYS * BPD)
    f = np.zeros(S["n"])
    ok = ~np.isnan(m)
    f[ok] = (S["c"][ok] > m[ok]).astype(float)
    return f


def build(S):
    base = supertrend(S["h"], S["l"], S["c"]) * daily_filter(S)
    cond = np.asarray(rsi_tv(S["c"], RSI_N) > RSI_T, bool)
    cond[:60] = False                       # RSI 가 덜 데워진 앞머리는 안 쓴다
    return base, apply_wait(base, cond), cond


def main():
    print(__doc__.strip())
    print("\n" + "=" * 118)
    mins, worsts = {}, {}
    for name, path in FILES.items():
        S = load(path)
        base, line, cond = build(S)
        print("\n[%s] %s ~ %s · %d봉 · 편도 %.1f%%"
              % (name, S["d"][0][:10], S["d"][-1][:10], S["n"], FEE * 100))
        report(S, np.ones(S["n"]), "그냥 보유")
        c0, m0 = report(S, base, "봇 (RSI 없음)")
        c1, m1 = report(S, line, "봇 + ta.rsi(14)>78 첫봉")
        # 같은 평균 노출로 비중만 줄여 늘 들고 있는 계좌 — 반론 기준선
        p0, _ = run(S, base); p1, _ = run(S, line)
        w = p1[WARM:].mean() / p0[WARM:].mean()
        report(S, base * w, "같은노출 고정비중 w=%.2f" % w)
        mins.setdefault("봇", []).append(c0); worsts.setdefault("봇", []).append(m0)
        mins.setdefault("한줄", []).append(c1); worsts.setdefault("한줄", []).append(m1)
        print("     RSI(%d)>%d 인 봉 %.2f%% · 신호가 켜진 봉 중 %.1f%%"
              % (RSI_N, RSI_T, cond[WARM:].mean() * 100, cond[base > 0].mean() * 100))

    print("\n" + "=" * 118)
    print("두 시장 min 연복리 / worst 최대낙폭  (잣대: 연 30% 이상 · 낙폭 -35% 이내)")
    for k in ("봇", "한줄"):
        mc, wm = min(mins[k]), min(worsts[k])
        print("  %-10s %6.1f%% / %6.1f%%   %s"
              % (k, mc * 100, wm * 100,
                 "통과" if (mc >= 0.30 and wm >= -0.35) else "탈락"))

    print("\n비용을 올리면 (두 시장 min 연복리)")
    for fee in (0.001, 0.002, 0.003, 0.005):
        out = []
        for name, path in FILES.items():
            S = load(path)
            base, line, _ = build(S)
            pos, g = run(S, line, fee)
            out.append(span(g[WARM:], S["ppy"])[0])
        print("  편도 %.1f%% — %5.1f%%  %s" % (fee * 100, min(out) * 100,
                                              "통과" if min(out) >= 0.30 else "탈락"))

    print("\n※ 이 페이지의 모든 수치는 과거 데이터 시뮬레이션이며 투자 권유가 아닙니다.")


if __name__ == "__main__":
    main()

시세 파일: BTCUSDT_4h.csv · KRW-BTC_4h.csv (같은 폴더의 data_rsi/ 안에 두세요)

직접 돌리기 어려우시면

한 줄을 아는 것과 6.83년 동안 그대로 지키는 것은 다른 일입니다. 이 계좌는 시장 안에 있던 시간이 10% 뿐이었습니다 — 나머지 시간은 아무것도 안 하고 기다린 시간입니다. 사람은 그 시간을 못 견딥니다. 알고랩은 전략 구현부터 자동매매 운영까지 대신해 드립니다.

알고랩에 맡기기 알고랩 소개 →

본 페이지의 모든 수치는 과거 데이터 기반 시뮬레이션이며 투자 권유가 아닙니다. 특정 종목·전략의 미래 수익을 보장하지 않습니다. 투자 판단과 책임은 투자자 본인에게 있습니다.