트레이딩뷰에서 많이 쓰이는 지표 13종을 설정값·일봉 필터까지 바꿔 888개 조합으로 만들고, 업비트 원화와 바이낸스 달러 두 거래소 9년(2017-12~2026-08)에서 전부 백테스트했습니다(1,776번). 잣대(두 거래소 모두 연복리 30% 이상 · 최대낙폭 -35% 이내 · 편도 0.1% 비용)를 통과한 조합은 4개였습니다. 이 페이지에서 그 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 같은 구간 | |||
이더리움 조합은 수익은 컸지만 낙폭이 번번이 잣대를 넘었습니다. 4개 전부 BTC.
종가가 일봉 이동평균 위일 때만 진입하는 조건 한 줄. 생존 슈퍼트렌드는 2018·2022 하락장 수익률이 정확히 0 — 그 해에 거래 자체를 안 했습니다.
ATR 밴드·채널을 따라가는 추세추종 계열만 남았습니다. RSI·볼린저 같은 평균회귀 192개 조합은 전멸.
생존 조합도 바로 옆 설정값은 아슬아슬하게 떨어졌습니다. 비용을 2배(편도 0.2%)로 올려도 연 44~50%는 유지했지만, 잣대가 빡셀 뿐 과신은 금물입니다.
위 4개 조합의 진입·청산 신호를 차트에 그대로 그립니다. 무료입니다.
트레이딩뷰에서 BTC 4시간봉 차트를 엽니다(업비트 KRW-BTC 또는 바이낸스 BTCUSDT). 다른 시간봉에서는 검증과 다른 신호가 나옵니다.
Pine 에디터를 열고 아래 코드를 붙여넣은 뒤 차트에 추가.
알림은 봉 마감 시 1회로 설정하세요. 세팅별 진입·청산 알림 8종이 들어 있습니다.
//@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 ④: 청산 신호")
파이썬 하나로 데이터 수집부터 전체 스윕까지. 기본 실행은 생존 4개 재현(몇 분),
--full 은 888조합 × 2거래소 전체(수십 분)입니다.
pip install numpy 만 있으면 됩니다.
python tv_indicator_backtest.py — 생존 4개 + 착시 사례 + 보유 재현.
python tv_indicator_backtest.py --full — 1,776번 전체.
# -*- 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()
매매는 연 12~33번, 4시간봉 캔들 마감은 새벽에도 옵니다. 2018·2022년에는 1년 내내 신호 없이 기다려야 했고, 한 번이라도 끼어들면 '잃지 않은 해'의 방어가 사라집니다. 그래서 저희는 검증된 규칙을 봇에게 맡깁니다.
※ 본 페이지의 수치는 공개된 시장 데이터를 과거 기준으로 검증한 교육·검증 목적의 자료이며, 특정 종목·전략의 매수·매도 권유가 아닙니다. 과거 백테스트 성과는 미래 수익을 보장하지 않습니다. 지표 파일과 코드는 무료이며, 사용에 따른 모든 투자 판단과 책임은 이용자 본인에게 있습니다.