트레이딩뷰 /scripts 피드에서 인기 상위에 있던 아이디어 9개를 골라 규칙을 직접 구현하고, 설정값·일봉 필터까지 바꿔 448개 조합으로 만들었습니다. 업비트 원화와 바이낸스 달러 두 거래소 9년에서 각각 돌렸으니 백테스트 896번입니다. 두 거래소를 동시에 통과한 조합은 10개였고, 전부 같은 아이디어 하나였습니다.
편마다 잣대를 바꾸면 편끼리 비교가 깨집니다. 지난 편(인기 지표 888조합)과 기준일·비용·관문을 한 글자도 바꾸지 않았습니다.
| 개념 | 어디서 왔나 | 조합 | 통과 | 최고 칸 min 연복리 | 그때 최악 낙폭 |
|---|---|---|---|---|---|
| 압축 돌파 | Consolidation 계열 | 64칸 | 10칸 | 50.7% | -33.8% |
| 유동성 스윕 반전 | TV 피드 1위 개념(부스트 3,278) | 72칸 | 0칸 | 0.4% | -57.9% |
| FVG 되돌림 | SMC 계열 상시 인기 | 48칸 | 0칸 | 55.9% | -46.2% |
| 편차채널 RSI | TV 피드(부스트 692) 개념 | 72칸 | 0칸 | 7.5% | -26.2% |
| 구조 전환 BOS | SMC 구조 전환 | 32칸 | 0칸 | 55.6% | -46.6% |
| 피벗 채널 돌파 | TV 피드 1위 개념(부스트 784 · 2026-09-05) | 32칸 | 0칸 | 59.5% | -67.5% |
| 반전 밀집 구역 | TV 피드 2위 개념(부스트 433) | 32칸 | 0칸 | 6.0% | -35.8% |
| 모멘텀 시퀀스 | TV 피드 4위 개념(부스트 235) | 48칸 | 0칸 | 36.8% | -53.7% |
| 충격 백분위 MA | TV 피드 7위 개념(부스트 101) | 48칸 | 0칸 | 60.3% | -63.8% |
'어디서 왔나'의 부스트 수는 그 개념을 채집한 날 트레이딩뷰 피드 기준입니다. 개별 스크립트를 지목하지 않습니다 — 남의 코드를 옮기지 않고 개념의 일반형을 우리가 다시 구현했고, 탈락은 집합 통계로만 다룹니다.
FVG 되돌림 ETH (look 20 · exit_n 20 · 일봉SMA50) — 업비트에서 461.9배 (1,000만원이 46억 1,922만원), 연복리 101.7%. 업비트만 보면 최대낙폭도 -33.8%로 잣대 안이었습니다. 그런데 같은 규칙을 바이낸스에서 돌리니 최대낙폭이 -53.4%로 벌어져 탈락했습니다.
한 거래소에서만 통하는 성적은 실력이 아니라 그 시장이 준 운입니다. 두 거래소를 동시에 통과시키는 관문이 있는 이유입니다.
| 조합 | 업비트 원화 | 바이낸스 달러 | ||||||
|---|---|---|---|---|---|---|---|---|
| 연복리 | 최대낙폭 | 배수 | 검증(2024~) | 연복리 | 최대낙폭 | 배수 | 검증(2024~) | |
| ETH (q 0.3 · brk_n 40 · exit_n 20 · 일봉필터 없음) | 65.6% | -29.0% | 82.4배 | 39.3% | 50.7% | -33.8% | 37.8배 | 23.0% |
| BTC (q 0.3 · brk_n 20 · exit_n 50 · 일봉SMA50) | 47.0% | -25.6% | 29.1배 | 18.5% | 46.7% | -28.7% | 29.9배 | 21.0% |
| ETH (q 0.3 · brk_n 40 · exit_n 20 · 일봉SMA50) | 51.6% | -32.4% | 38.1배 | 41.8% | 41.2% | -28.9% | 21.3배 | 29.7% |
| BTC (q 0.3 · brk_n 20 · exit_n 20 · 일봉SMA50) | 41.6% | -24.7% | 21.0배 | 16.9% | 35.1% | -32.9% | 14.3배 | 18.3% |
| BTC (q 0.3 · brk_n 20 · exit_n 50 · 일봉SMA200) | 43.7% | -25.6% | 23.9배 | 22.6% | 34.3% | -23.5% | 13.7배 | 18.7% |
| BTC (q 0.2 · brk_n 20 · exit_n 50 · 일봉SMA100) | 41.2% | -30.2% | 20.4배 | 17.4% | 32.7% | -33.4% | 12.2배 | 17.5% |
| ETH (q 0.2 · brk_n 40 · exit_n 20 · 일봉필터 없음) | 46.1% | -22.5% | 27.6배 | 25.1% | 32.1% | -31.3% | 11.8배 | 3.3% |
| ETH (q 0.3 · brk_n 40 · exit_n 20 · 일봉SMA100) | 33.3% | -29.0% | 12.4배 | 24.2% | 31.3% | -29.1% | 11.1배 | 16.7% |
| BTC (q 0.2 · brk_n 20 · exit_n 20 · 일봉SMA50) | 31.4% | -19.9% | 10.9배 | 20.2% | 31.1% | -28.0% | 11.0배 | 19.2% |
| BTC (q 0.2 · brk_n 20 · exit_n 50 · 일봉SMA200) | 41.8% | -23.8% | 21.2배 | 21.3% | 30.0% | -23.2% | 10.2배 | 18.8% |
| 2017 | 2018 | 2019 | 2020 | 2021 | 2022 | 2023 | 2024 | 2025 | 2026 |
|---|---|---|---|---|---|---|---|---|---|
| -10.7% | 158.0% | 61.9% | 272.8% | 128.1% | 4.0% | 3.1% | 57.9% | 38.7% | 10.6% |
연도별 수익률(업비트 · 비용 반영). 같은 구간 그냥 보유는 6.1배(연복리 22.9% · 최대낙폭 -95.9%)였습니다.
데이터는 업비트·바이낸스 공개 API 로 직접 받습니다. 영상과 같은 숫자가
나오도록 기준일(ASOF)이 고정돼 있습니다.
# -*- coding: utf-8 -*-
"""트레이딩뷰 커뮤니티 '신상' 스크립트 개념 9종 × 448조합 × 두 거래소 검증
— 영상 재현 코드 (AlgoLab)
- 기본 실행: 잣대를 통과한 조합 + 인기 상위 개념의 성적 + 그냥 보유를 재현합니다 (몇 분).
- 전체 재현: `python tv_new_backtest.py --full` — 9개념 448조합 × 업비트·바이낸스
= 896번 백테스트 전부 (수십 분 걸립니다).
여기 있는 것은 **특정 스크립트의 이식이 아니라 개념의 일반형을 자체 구현한 것**입니다.
트레이딩뷰 /scripts 피드에서 그날 인기 상위에 있던 아이디어를 읽고, 규칙을 우리가 다시
썼습니다. 남의 코드를 옮기지 않았고, 개별 스크립트를 저격하지도 않습니다.
데이터: 업비트 원화(KRW-BTC/ETH)·바이낸스 달러(BTCUSDT/ETHUSDT) 4시간봉, 공개 API.
처음 실행하면 내려받아 CSV 로 저장하고, 다음부터는 재사용합니다.
⚠ ASOF: 영상 제작 시점까지로 데이터를 잘라야 영상과 같은 숫자가 나옵니다.
오늘까지 보려면 ASOF = None 으로 바꾸세요(숫자는 영상과 달라집니다).
지난 편(인기 지표 888조합)과 **같은 기준일·같은 잣대**를 씁니다. 그래야 두 편의 수치를
나란히 비교할 수 있습니다.
⚠ 이 코드는 시뮬레이션입니다. 4시간봉 종가 체결·편도 0.1% 비용을 가정했고 슬리피지는
반영하지 않았습니다. 과거 성적은 미래 수익을 보장하지 않습니다.
"""
import csv, datetime, json, os, sys, time, urllib.parse, urllib.request
import numpy as np
ASOF = "2026-08-31T12:00:00" # 영상 제작 기준 마지막 봉(UTC). None 이면 오늘까지.
COST = 0.001 # 편도 0.1%
WARM = 400 # 워밍업 봉 수(지표가 자리 잡기 전 구간은 성적에서 뺀다)
VAL_START = "2024-01-01" # 검증 구간 시작
HERE = os.path.dirname(os.path.abspath(__file__))
BARS_PER_DAY = 6 # 4시간봉
# ── 데이터 수집 (공개 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(c, n):
out = np.empty_like(c, dtype=float)
k = 2 / (n + 1)
out[0] = c[0]
for i in range(1, len(c)):
out[i] = c[i] * k + out[i-1] * (1 - k)
return out
def _atr(h, l, c, n=14):
pc = np.concatenate(([c[0]], c[:-1]))
tr = np.maximum(h - l, np.maximum(np.abs(h - pc), np.abs(l - pc)))
return _ema(tr, n)
# ══ 개념 9종 — 전부 '그 봉 종가까지의 정보'만 씁니다(다음 봉 반영은 러너가 처리) ══
def sweep_reversal(o, h, l, c, n=20, exit_n=20):
"""유동성 스윕 반전 — 직전 n봉 최저를 아래로 찔렀다가 종가가 그 위로 복귀하면 매수.
청산: 종가 < EMA(exit_n)."""
N = len(c); ema = _ema(c, exit_n); sig = np.zeros(N); pos = 0.0
for t in range(n + 1, N):
prior_low = l[t-n:t].min()
if pos == 0.0:
if l[t] < prior_low and c[t] > prior_low:
pos = 1.0
elif c[t] < ema[t]:
pos = 0.0
sig[t] = pos
return sig
def fvg_pullback(o, h, l, c, look=30, exit_n=20):
"""상승 FVG(3봉 갭) 생성 후 look봉 안에 갭 구간으로 되돌아오면 매수."""
N = len(c); ema = _ema(c, exit_n); sig = np.zeros(N); pos = 0.0
z_top = z_bot = np.nan; z_age = 10**9
for t in range(2, N):
if pos == 0.0:
if l[t] > h[t-2]:
z_top, z_bot, z_age = l[t], h[t-2], 0
else:
z_age += 1
if z_age <= look and not np.isnan(z_top):
if l[t] <= z_top and c[t] > z_bot:
pos = 1.0; z_age = 10**9
else:
if (not np.isnan(z_bot) and c[t] < z_bot) or c[t] < ema[t]:
pos = 0.0; z_top = z_bot = np.nan
sig[t] = pos
return sig
def devchan_rsi(o, h, l, c, n=120, k=2.0, rsi_n=14):
"""회귀 편차 채널 + RSI 트리거 — n봉 선형회귀 ± k×잔차표준편차의 하단에서 매수."""
N = len(c); sig = np.zeros(N)
x = np.arange(n, dtype=float); sx, sxx = x.sum(), (x*x).sum()
denom = n * sxx - sx * sx
d = np.diff(c, prepend=c[0])
up = np.where(d > 0, d, 0.0); dn = np.where(d < 0, -d, 0.0)
rsi = 100 - 100 / (1 + _ema(up, 2*rsi_n-1) / np.maximum(_ema(dn, 2*rsi_n-1), 1e-12))
pos = 0.0
for t in range(n, N):
y = c[t-n+1:t+1]; sy = y.sum(); sxy = (x*y).sum()
slope = (n*sxy - sx*sy) / denom
fit = (sy - slope*sx)/n + slope*x
sd = (y - fit).std(); mid = fit[-1]
if pos == 0.0:
if c[t] < mid - k*sd and rsi[t] > rsi[t-1]:
pos = 1.0
elif c[t] >= mid:
pos = 0.0
sig[t] = pos
return sig
def squeeze_break(o, h, l, c, q=0.25, brk_n=20, exit_n=20, bb_n=20, win=252):
"""압축 돌파 — 밴드폭(20봉 표준편차÷평균)이 최근 win봉 하위 q 분위면 '압축'.
압축 직후 5봉 안에 직전 brk_n봉 고가를 종가로 넘으면 매수. 청산: 종가 < EMA(exit_n)."""
N = len(c); ema = _ema(c, exit_n)
width = np.full(N, np.nan)
for t in range(bb_n, N):
seg = c[t-bb_n+1:t+1]
width[t] = seg.std() / max(seg.mean(), 1e-12)
sig = np.zeros(N); pos = 0.0; armed = -10**9
for t in range(bb_n + win, N):
if width[t] <= np.nanquantile(width[t-win+1:t+1], q):
armed = t
if pos == 0.0:
if t - armed <= 5 and c[t] > h[t-brk_n:t].max():
pos = 1.0
elif c[t] < ema[t]:
pos = 0.0
sig[t] = pos
return sig
def bos_trend(o, h, l, c, piv=5):
"""구조 전환(BOS) — 확정 스윙 고점 위로 마감하면 매수, 확정 스윙 저점 아래면 청산."""
N = len(c); sig = np.zeros(N); last_hi = last_lo = np.nan; pos = 0.0
for t in range(2*piv, N):
i = t - piv
if h[i] == h[i-piv:i+piv+1].max():
last_hi = h[i]
if l[i] == l[i-piv:i+piv+1].min():
last_lo = l[i]
if pos == 0.0:
if not np.isnan(last_hi) and c[t] > last_hi:
pos = 1.0
elif not np.isnan(last_lo) and c[t] < last_lo:
pos = 0.0
sig[t] = pos
return sig
def pivot_channel(o, h, l, c, piv=5, atr_k=0.0):
"""피벗 채널 추세선 돌파 — 확정 스윙 고점 두 개를 이어 연장한 선 위로 마감하면 매수,
저점 쪽 연장선 아래로 마감하면 청산. atr_k>0 이면 ATR 필터로 잔챙이 구조를 거른다."""
N = len(c); atr = _atr(h, l, c, 14); sig = np.zeros(N); pos = 0.0
hi1 = hi2 = lo1 = lo2 = None
for t in range(2*piv, N):
i = t - piv
if h[i] == h[i-piv:i+piv+1].max():
if hi2 is None or i != hi2[0]:
hi1, hi2 = hi2, (i, h[i])
if l[i] == l[i-piv:i+piv+1].min():
if lo2 is None or i != lo2[0]:
lo1, lo2 = lo2, (i, l[i])
up = dn = np.nan
if hi1 is not None and hi2 is not None and hi2[0] > hi1[0]:
if atr_k == 0.0 or abs(hi2[1]-hi1[1]) >= atr_k*atr[t]:
s = (hi2[1]-hi1[1]) / (hi2[0]-hi1[0]); up = hi2[1] + s*(t-hi2[0])
if lo1 is not None and lo2 is not None and lo2[0] > lo1[0]:
if atr_k == 0.0 or abs(lo2[1]-lo1[1]) >= atr_k*atr[t]:
s = (lo2[1]-lo1[1]) / (lo2[0]-lo1[0]); dn = lo2[1] + s*(t-lo2[0])
if pos == 0.0:
if not np.isnan(up) and c[t] > up:
pos = 1.0
elif not np.isnan(dn) and c[t] < dn:
pos = 0.0
sig[t] = pos
return sig
def reversal_profile(o, h, l, c, look=300, bins=30, piv=5, exit_n=20):
"""반전 밀집 프로파일 — 확정 피벗 가격을 최근 look봉 범위에 bins칸으로 쌓아 가장
밀집한 가격대를 찾는다. 종가가 그 구역 안이고 직전봉보다 오르면 매수."""
N = len(c); ema = _ema(c, exit_n); sig = np.zeros(N); pos = 0.0
pv_i, pv_p = [], []; zlo = zhi = np.nan
for t in range(2*piv, N):
i = t - piv
if h[i] == h[i-piv:i+piv+1].max():
pv_i.append(i); pv_p.append(h[i])
if l[i] == l[i-piv:i+piv+1].min():
pv_i.append(i); pv_p.append(l[i])
if t % 6 == 0 and pv_i:
ai = np.asarray(pv_i); ap = np.asarray(pv_p)
ps = ap[ai >= t - look]
if len(ps) >= 5:
lo_, hi_ = ps.min(), ps.max()
if hi_ > lo_:
cnt, edge = np.histogram(ps, bins=bins, range=(lo_, hi_))
j = int(cnt.argmax()); zlo, zhi = edge[j], edge[j+1]
if pos == 0.0:
if not np.isnan(zlo) and zlo <= c[t] <= zhi and c[t] > c[t-1]:
pos = 1.0
elif c[t] < ema[t]:
pos = 0.0
sig[t] = pos
return sig
def momentum_sequence(o, h, l, c, seq=4, exit_seq=2):
"""모멘텀 시퀀스 — 이동평균을 전혀 쓰지 않는다. 종가가 seq봉 연속 오르면 매수,
exit_seq봉 연속 내리면 청산."""
N = len(c); sig = np.zeros(N); pos = 0.0; up = dn = 0
for t in range(1, N):
if c[t] > c[t-1]:
up += 1; dn = 0
elif c[t] < c[t-1]:
dn += 1; up = 0
else:
up = dn = 0
if pos == 0.0:
if up >= seq:
pos = 1.0
elif dn >= exit_seq:
pos = 0.0
sig[t] = pos
return sig
def spma_trend(o, h, l, c, n=50, pct_=0.8, win=252):
"""충격 백분위 이동평균 — 보통 이동평균은 매 봉 따라가지만, 이 선은 변동폭이 최근
win봉 상위 구간에 드는 '충격' 봉에서만 가격 쪽으로 움직인다. 종가가 선 위면 매수."""
N = len(c)
prev = np.concatenate(([c[0]], c[:-1]))
r = np.abs(np.diff(c, prepend=c[0]) / np.maximum(np.abs(prev), 1e-12))
line = np.empty(N); line[0] = c[0]
k = 2 / (n + 1); sig = np.zeros(N)
for t in range(1, N):
lo = max(0, t - win + 1)
thr = np.quantile(r[lo:t+1], pct_)
line[t] = line[t-1] + k*(c[t]-line[t-1]) if r[t] >= thr else line[t-1]
if t < win:
continue
sig[t] = 1.0 if c[t] > line[t] else 0.0
return sig
POOL = {
"유동성 스윕 반전": (sweep_reversal,
[{"n": n, "exit_n": e} for n in (10, 20, 40) for e in (10, 20, 50)]),
"FVG 되돌림": (fvg_pullback,
[{"look": lk, "exit_n": e} for lk in (20, 40) for e in (10, 20, 50)]),
"편차채널 RSI": (devchan_rsi,
[{"n": n, "k": k} for n in (80, 120, 200) for k in (1.5, 2.0, 2.5)]),
"압축 돌파": (squeeze_break,
[{"q": q, "brk_n": b, "exit_n": e} for q in (0.2, 0.3) for b in (20, 40) for e in (20, 50)]),
"구조 전환 BOS": (bos_trend, [{"piv": p} for p in (3, 5, 8, 12)]),
"피벗 채널 돌파": (pivot_channel,
[{"piv": p, "atr_k": a} for p in (5, 10) for a in (0.0, 1.0)]),
"반전 밀집 구역": (reversal_profile,
[{"look": lk, "bins": b} for lk in (200, 400) for b in (20, 40)]),
"모멘텀 시퀀스": (momentum_sequence,
[{"seq": s, "exit_seq": e} for s in (3, 4, 5) for e in (2, 3)]),
"충격 백분위 MA": (spma_trend,
[{"n": n, "pct_": p} for n in (20, 50, 100) for p in (0.7, 0.85)]),
}
FILTS = (0, 50, 100, 200) # 일봉 추세필터: 없음 / SMA50 / 100 / 200 (4시간봉 × 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 * BARS_PER_DAY)
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]]
return (1 + pos * S["ret"]) * (1 - cost * np.abs(np.diff(np.r_[0.0, pos])))
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):
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}%"
# 잣대를 통과한 조합 — 영상에서 공개한 그대로. **10개가 전부 같은 개념입니다.**
# (개념, 함수, 자산, 파라미터, 일봉필터)
SURVIVORS = [
("압축 돌파", squeeze_break, "ETH", {"q": 0.3, "brk_n": 40, "exit_n": 20}, 0),
("압축 돌파", squeeze_break, "BTC", {"q": 0.3, "brk_n": 20, "exit_n": 50}, 50),
("압축 돌파", squeeze_break, "ETH", {"q": 0.3, "brk_n": 40, "exit_n": 20}, 50),
("압축 돌파", squeeze_break, "BTC", {"q": 0.3, "brk_n": 20, "exit_n": 20}, 50),
("압축 돌파", squeeze_break, "BTC", {"q": 0.3, "brk_n": 20, "exit_n": 50}, 200),
("압축 돌파", squeeze_break, "BTC", {"q": 0.2, "brk_n": 20, "exit_n": 50}, 100),
("압축 돌파", squeeze_break, "ETH", {"q": 0.2, "brk_n": 40, "exit_n": 20}, 0),
("압축 돌파", squeeze_break, "ETH", {"q": 0.3, "brk_n": 40, "exit_n": 20}, 100),
("압축 돌파", squeeze_break, "BTC", {"q": 0.2, "brk_n": 20, "exit_n": 20}, 50),
("압축 돌파", squeeze_break, "BTC", {"q": 0.2, "brk_n": 20, "exit_n": 50}, 200),
]
# 그날 커뮤니티 인기 상위 개념의 '최고 칸' + 최고 배수 탈락자
# (개념, 함수, 자산, 파라미터, 일봉필터, 설명)
POPULAR = [
("피벗 채널 돌파", pivot_channel, "ETH", {"piv": 10, "atr_k": 0.0}, 0, "TV 피드 1위 개념(부스트 784 · 2026-09-05)"),
("반전 밀집 구역", reversal_profile, "ETH", {"look": 400, "bins": 20}, 0, "TV 피드 2위 개념(부스트 433)"),
("모멘텀 시퀀스", momentum_sequence, "BTC", {"seq": 4, "exit_seq": 3}, 50, "TV 피드 4위 개념(부스트 235)"),
("유동성 스윕 반전", sweep_reversal, "BTC", {"n": 10, "exit_n": 50}, 50, "TV 피드 1위 개념(부스트 3,278)"),
("충격 백분위 MA", spma_trend, "ETH", {"n": 100, "pct_": 0.7}, 0, "TV 피드 7위 개념(부스트 101)"),
("FVG 되돌림", fvg_pullback, "ETH", {"look": 20, "exit_n": 20}, 50, "최고 배수 탈락자"),
]
def main():
full = "--full" in sys.argv
print(f"기준일 ASOF = {ASOF or '오늘'} · 비용 편도 {COST*100:.1f}% · 4시간봉")
print(f"잣대: 업비트·바이낸스 두 시장 모두 연복리 30% 이상 · 최대낙폭 -35% 이내\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"))
A = {
"BTC": (series(os.path.join(HERE, "upbit_btc_4h.csv")),
series(os.path.join(HERE, "binance_btc_4h.csv"))),
"ETH": (series(os.path.join(HERE, "upbit_eth_4h.csv")),
series(os.path.join(HERE, "binance_eth_4h.csv"))),
}
Su = A["BTC"][0]
print(f"업비트 BTC {Su['d'][0][:10]} ~ {Su['d'][-1][:10]} ({Su['n']:,}봉)\n")
print("=" * 76)
print("① 잣대를 통과한 조합 (영상의 생존자)")
print("=" * 76)
for name, fn, asset, param, filt in SURVIVORS:
Au, Ab = A[asset]
u, b, mn, wm, ok = cell(Au, Ab, fn, param, filt)
setting = " · ".join(f"{k} {v}" for k, v in param.items())
print(f"\n{name} {asset} ({setting}) + 일봉SMA{filt}" if filt
else f"\n{name} {asset} ({setting}) · 일봉필터 없음")
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" + "=" * 76)
print("② 인기 상위 개념의 최고 칸 · 그리고 가장 많이 벌고도 떨어진 조합")
print("=" * 76)
for name, fn, asset, param, filt, why in POPULAR:
Au, Ab = A[asset]
u, b, mn, wm, ok = cell(Au, Ab, fn, param, filt)
print(f" {name:<12} {asset} {param} SMA{filt}")
print(f" ({why}) min 연복리 {pct(mn)}"
f" | 업 {u['mult']:.1f}배/{pct(u['mdd'])} | 바 {b['mult']:.1f}배/{pct(b['mdd'])}"
f" → {'통과' if ok else '탈락'}")
print("\n" + "=" * 76)
print("③ 그냥 보유 (같은 구간·업비트)")
print("=" * 76)
for asset, (Au, _) in A.items():
hc, hm, hx = stats(1 + Au["ret"][WARM:], Au["ppy"])
print(f" {asset}: 연복리 {pct(hc)} · 최대낙폭 {pct(hm)} · {hx:.1f}배")
if not full:
print("\n(전체 448조합 × 2거래소 = 896번 재현: --full 옵션 · 수십 분)")
return
print("\n" + "=" * 76)
print("④ 전체 스윕 — 9개념 448조합 × 업비트·바이낸스")
print("=" * 76)
n_all = n_pass = n_ret = 0
passed = []
for strat, (fn, params) in POOL.items():
for asset, (Au, Ab) in A.items():
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()
통과한 조합의 신호를 차트에 그대로 그립니다. 트레이딩뷰 Pine 에디터에 붙여 넣으면 바로 돌아갑니다.
//@version=5
// ============================================================================
// AlgoLab Verified — 압축 돌파 (Squeeze Breakout) · 배포판 v2 · 2026-09-05
//
// 트레이딩뷰 커뮤니티 '신상' 스크립트에서 채집한 개념 9종 × 448개 조합을
// 업비트(원화) + 바이낸스(USDT) 9년으로 전부 백테스트했습니다. 두 시장 동시 잣대
// (연복리 30% 이상 · 최대낙폭 -35% 이내 · 편도 비용 0.1%)를 통과한 조합은 10개였고,
// 10개가 전부 이 '압축 돌파' 한 아이디어였습니다. 이 지표는 그 신호를 그대로 그립니다.
// 재현 코드·전체 수치: https://algolab.co.kr/newtv
//
// ① ETH 4H 압축분위 30% / 돌파 40봉 / 청산 EMA20 · 일봉필터 없음
// 업비트 연 65.6% · 최대낙폭 -29.0% · 82.4배 | 바이낸스 연 50.7% · -33.8% · 37.8배
// ② BTC 4H 압축분위 30% / 돌파 20봉 / 청산 EMA50 · 일봉 SMA50 필터
// 업비트 연 47.0% · 최대낙폭 -25.6% · 29.1배 | 바이낸스 연 46.7% · -28.7% · 29.9배
//
// ⚠ 반드시 읽을 것
// · **4시간봉 차트 전용**입니다. 일봉 필터를 4시간봉 개수로 환산(하루 6봉)해 계산하므로
// 다른 시간봉에서는 검증과 다른 신호가 나옵니다.
// · 검증 구간은 2017-12 ~ 2026-08, **전 구간 최적화** 성적이며 미래를 보장하지 않습니다.
// 2024년 이후만 자르면 업비트 연 39.3%, 바이낸스 연 23.0%로 낮아집니다.
// · 신호는 봉 마감 기준입니다. 알림은 "봉 마감 시 1회"로 설정하세요.
// · 투자 권유가 아닙니다. 판단과 책임은 사용자 본인에게 있습니다.
// ============================================================================
indicator("AlgoLab Verified — 압축 돌파 (Squeeze Breakout)", overlay=true,
max_labels_count=500)
// ── 입력 ────────────────────────────────────────────────────────────────────
preset = input.string("① ETH 4H (검증 설정)", "프리셋",
options=["① ETH 4H (검증 설정)", "② BTC 4H (검증 설정)", "직접 설정"])
bbN = input.int(20, "밴드폭 계산 봉수", minval=5, group="직접 설정")
winN = input.int(252, "분위수 창(봉)", minval=60, group="직접 설정")
qPct = input.float(30, "압축 분위(%)", minval=1, maxval=90, group="직접 설정")
brkN = input.int(40, "돌파 기준 봉수", minval=5, group="직접 설정")
exitN = input.int(20, "청산 EMA 기간", minval=2, group="직접 설정")
filtDay = input.int(0, "일봉 추세필터 SMA(0=없음)", minval=0, group="직접 설정")
armWin = input.int(5, "압축 뒤 유효 봉수", minval=1, group="직접 설정")
showBg = input.bool(true, "압축 구간 배경 표시")
// 프리셋이 직접 설정을 덮는다 — 검증된 조합을 실수로 바꾸지 않게
useN = preset == "② BTC 4H (검증 설정)" ? 20 : preset == "직접 설정" ? bbN : 20
useWin = preset == "직접 설정" ? winN : 252
useQ = preset == "직접 설정" ? qPct : 30.0
useBrk = preset == "① ETH 4H (검증 설정)" ? 40 : preset == "② BTC 4H (검증 설정)" ? 20 : brkN
useExit = preset == "① ETH 4H (검증 설정)" ? 20 : preset == "② BTC 4H (검증 설정)" ? 50 : exitN
useFilt = preset == "① ETH 4H (검증 설정)" ? 0 : preset == "② BTC 4H (검증 설정)" ? 50 : filtDay
useArm = preset == "직접 설정" ? armWin : 5
// ── 계산 ────────────────────────────────────────────────────────────────────
// 밴드폭 = 종가 표준편차 ÷ 종가 평균 (같은 봉수). 값이 작을수록 눌린 구간이다.
width = ta.stdev(close, useN) / ta.sma(close, useN)
// 최근 useWin 봉 중 하위 useQ% 문턱 (백테스트의 np.nanquantile 과 같은 선형보간)
thresh = ta.percentile_linear_interpolation(width, useWin, useQ)
squeezed = width <= thresh
// 압축이 관측된 마지막 봉
var int armedBar = na
if squeezed
armedBar := bar_index
// 일봉 추세필터 — 4시간봉 개수로 환산(하루 6봉)
filtLen = useFilt * 6
trendOK = useFilt == 0 or (not na(ta.sma(close, filtLen)) and close > ta.sma(close, filtLen))
brkLevel = ta.highest(high, useBrk)[1] // 직전 useBrk 봉 고가(현재 봉 제외)
exitLine = ta.ema(close, useExit)
var bool inPos = false
armedOK = not na(armedBar) and (bar_index - armedBar) <= useArm
buySig = not inPos and armedOK and close > brkLevel and trendOK
sellSig = inPos and close < exitLine
if buySig
inPos := true
else if sellSig
inPos := false
// ── 그리기 ──────────────────────────────────────────────────────────────────
bgcolor(showBg and squeezed ? color.new(color.gray, 88) : na)
plot(exitLine, "청산선 EMA", color=color.new(color.orange, 30), linewidth=1)
plot(brkLevel, "돌파선", color=color.new(color.teal, 55), style=plot.style_stepline)
plotshape(buySig, "매수", shape.labelup, location.belowbar,
color=color.new(color.teal, 0), text="매수", textcolor=color.white, size=size.tiny)
plotshape(sellSig, "청산", shape.labeldown, location.abovebar,
color=color.new(color.red, 10), text="청산", textcolor=color.white, size=size.tiny)
barcolor(inPos ? color.new(color.teal, 60) : na)
alertcondition(buySig, "압축 돌파 매수", "압축 돌파 매수 신호 (봉 마감)")
alertcondition(sellSig, "압축 돌파 청산", "압축 돌파 청산 신호 (봉 마감)")
위 규칙은 4시간마다 봉이 닫힐 때 표준편차와 분위수를 다시 재고, 조건이 맞으면 바로 주문을 내야 합니다. 9년에 131번 — 하루에 여섯 번 오는 봉 마감을 몇 년 동안 사람이 지키는 건 불가능에 가깝습니다. 그래서 봇이 필요합니다.
알고랩은 이런 규칙을 코드로 옮기고 실계좌에 붙이는 일을 합니다. 전략을 숨기지 않습니다 — 위 코드는 전부 무료입니다. 직접 만들기 번거로우면 맡기시면 됩니다.
본 페이지는 정보 제공 목적의 백테스트 기록이며 투자 권유가 아닙니다. 과거 성적은 미래 수익을 보장하지 않습니다.