비트코인 선물 · 롱숏 · 실측 재현 페이지
선물 계좌에서는 롱도 숏도 잡을 수 있습니다. 그래서 많은 봇이 팔라는 신호에 롱을 숏으로 뒤집습니다. 같은 봇 신호를 두 계좌로 나눠 돌렸습니다 — 팔라는 신호에 현금으로 쉬는 계좌(롱만)와 숏으로 뒤집는 계좌(숏도). 바이낸스 USDT-M 선물 4시간봉, 2020년 2월 1일 ~ 2026년 10월 4일(6년 8개월), 1,000만원.
| 항목 | 값 |
|---|---|
| 방향 | 롱만 — 사라는 신호에 롱, 팔라는 신호에 롱을 정리하고 현금으로 쉰다(숏으로 뒤집지 않는다) |
| 보여 드린 봇 | 트레이딩뷰 슈퍼트렌드 기본값 — ATR 기간 10 · 배수 3 · 4시간봉 · 필터 없음 (결과를 보고 고른 값이 아니다) |
| 레버리지 | 1배 — 계좌만큼만 잡는다 |
| 비용 가정 | 수수료 편도 0.1% · 펀딩비 8시간 실이력 · 호가 밀림 없음 |
보여 드린 봇의 팔라는 신호 164번 중 106번은, 다시 사라는 신호가 날 때 가격이 판 값보다 높았습니다. 롱만 친 계좌는 그동안 현금이라 잃을 것이 없고, 숏도 친 계좌는 그만큼 잃습니다. 숏 구간에서 펀딩비는 오히려 받았습니다(계좌 대비 누적 +0.138) — 손실은 펀딩비가 아니라 가격에서 왔습니다.
덤 — 2020년 2월에 1배 숏을 잡고 들고만 있었다면 2020-12-16T12:00:00(UTC)에 강제청산됐습니다(가격 9,365 → 그 봉 고가 20,877 달러).
봇 13종(절대 모멘텀 · 이동평균 크로스 · 돈치안 · 슈퍼트렌드 · UT Bot · 헐MA · 켈트너 · 일목 · MACD · RSI 평균회귀 · 볼린저 평균회귀 · WaveTrend · Squeeze) 111설정 × 일봉 필터 두 가지(없음 / 4시간봉 1,200개 평균 위에서만) × 비트코인·이더리움 = 444개.
| 판정 | 개수 |
|---|---|
| 숏도 친 쪽이 돈을 더 남김 | 3 (롱만이 더 남김 441) |
| 숏도 친 쪽이 덜 떨어짐(고점 대비) | 1 |
| 돈도 더 남기고 덜 떨어짐 | 0 |
| 숏도 친 쪽이 강제청산 | 52 (전부 RSI·볼린저 평균회귀 — 신호가 꺼져 있는 시간이 길어 숏을 오래 든다) |
| 설정 | 롱만 (배수 · 가장 깊게) | 숏도 (배수 · 가장 깊게) |
|---|---|---|
| BTC 절대 모멘텀 {'look': 252} | 10.16배 · -50.8% | 11.44배 · -55.9% |
| BTC 이동평균 크로스 {'fast': 10, 'slow': 200} | 9.58배 · -43.2% | 10.81배 · -62.9% |
| ETH 절대 모멘텀 {'look': 189} | 30.45배 · -56.6% | 38.92배 · -70.5% |
보여 드린 슈퍼트렌드는 숏으로 잃은 비율로 비트코인 222개 중 덜 잃은 쪽부터 73% 자리, 444개 전체로는 83% 자리입니다 — 가운데보다 덜 잃은, 우리 주장에 불리한 예시입니다.
일봉 종가가 200일 평균선 아래인 날(전날 판정)에만 숏, 아니면 현금. 같은 444개를 다시 돌렸습니다. 롱만보다 돈을 더 남긴 설정은 125개로 늘었습니다 — 비트코인 222개 중 1개, 이더리움 222개 중 124개. 돈도 더 남기고 덜 떨어진 설정은 비트코인 0개 · 이더리움 10개입니다. 그래서 이 처방은 비트코인 선물 봇에만 드립니다 — 이더리움에서 하락장 숏이 남는지는 이 페이지가 답하지 않습니다.
돌리는 법: zip 을 풀고 pip install numpy 뒤 python run_longshort.py (1분 안팎).
[BTC] 2020-01-31T20:00:00 ~ 2026-10-04T16:00:00 (UTC) · 4시간봉 14,628개 · 빠진 봉 0
그냥 들고만 있었다면(1배 롱 · 수수료·펀딩 없이) 1,000만원 → 9,118만원 · 펀딩비가 플러스(롱이 내고 숏이 받음)였던 정산 86%
1배 숏을 2020-01-31 에 잡고 들고만 있었다면: 강제청산 2020-12-16T12:00:00 (가격 9,365 → 그날 고가 20,877)
[ETH] 2020-02-01T16:00:00 ~ 2026-10-04T16:00:00 (UTC) · 4시간봉 14,623개 · 빠진 봉 0
그냥 들고만 있었다면(1배 롱 · 수수료·펀딩 없이) 1,000만원 → 1억 4,757만원 · 펀딩비가 플러스(롱이 내고 숏이 받음)였던 정산 86%
1배 숏을 2020-02-01 에 잡고 들고만 있었다면: 강제청산 2020-08-02 (가격 183 → 그날 고가 409)
① 대표 봇 — 트레이딩뷰 슈퍼트렌드 기본값(ATR 10 · 배수 3) · 비트코인 · 필터 없음 · 1,000만원
기간 6.67년 (6년 8개월)
롱만(팔라는 신호에 현금) : 3,492만원 · 고점 대비 가장 깊게 -48.9%
숏도(팔라는 신호에 숏) : 1,236만원 · 고점 대비 가장 깊게 -62.3%
팔라는 신호 164번 중 다시 사라는 신호가 날 때 가격이 판 값보다 높았던 것 106번
숏 구간에서 받은 펀딩비(계좌 대비 누적) +0.138 — 받는 쪽이었다
체결 횟수 롱만 328번 · 숏도 656번
숏으로 잃은 비율(숏도÷롱만) 0.354 — 비트코인 222개 중 덜 잃은 쪽부터 73% 자리 (가운데 0.205)
444개 전체로 세면 덜 잃은 쪽부터 83% 자리
② 444개 전부 — 봇 13종 111설정 × 필터 2 × BTC·ETH = 444개
숏도가 돈을 더 남긴 것 3개 · 롱만이 더 남긴 것 441개
BTC 절대 모멘텀 {'look': 252} 필터0: 롱만 10.16배/-50.8% · 숏도 11.44배/-55.9% ← 숏도가 더 깊게 떨어짐
BTC 이동평균 크로스 {'fast': 10, 'slow': 200} 필터0: 롱만 9.58배/-43.2% · 숏도 10.81배/-62.9% ← 숏도가 더 깊게 떨어짐
ETH 절대 모멘텀 {'look': 189} 필터0: 롱만 30.45배/-56.6% · 숏도 38.92배/-70.5% ← 숏도가 더 깊게 떨어짐
숏도가 덜 떨어진 것 1개 · 돈도 더 남기고 덜 떨어진 것 0개
숏도에서 강제청산된 설정 52개 (전부 ['RSI 평균회귀', '볼린저 평균회귀'])
민감도 — 봉마다 다시 맞추는 계좌로 재면 숏도가 더 남긴 것 0개
③ 하락장일 때만 숏 — 일봉 종가가 200일 평균선 아래(전날 판정)인 날만 숏, 아니면 현금
롱만보다 돈을 더 남긴 것 125개 · 돈도 더 남기고 덜 떨어진 것 10개 (자산: ['ETH'])
BTC: 돈을 더 남긴 것 1개 / 222개 · 돈도 더 남기고 덜 떨어진 것 0개
ETH: 돈을 더 남긴 것 124개 / 222개 · 돈도 더 남기고 덜 떨어진 것 10개
# -*- coding: utf-8 -*-
"""비트코인 선물 봇 — 팔라는 신호에 숏으로 뒤집을까, 현금으로 쉴까 (AlgoLab 재현 코드)
pip install numpy
python run_longshort.py
같은 봇 신호를 두 방식으로 돌린다.
롱만(쉬기) : 사라는 신호에 롱, 팔라는 신호에 롱을 정리하고 현금
숏도(뒤집기): 팔라는 신호에 같은 크기로 숏
하락장만 : 팔라는 신호이고 일봉 종가가 200일 평균선 아래(전날 판정)일 때만 숏, 아니면 현금
봇 = strat_pool.py 의 13종 111설정 × 일봉 필터(없음 / 4시간봉 1,200개 평균 ≈ 200일선 위에서만) × BTC·ETH = 444개.
계좌 = 실제 선물처럼 **수량 고정 1배**: 진입 때 계좌 전부로 수량을 정하고 신호가 바뀔 때까지 수량을 안 바꾼다.
숏 손익 = 수량 × (진입가 − 지금가). 유지증거금 0.4% 아래면 강제청산(봉 고가·저가로 판정) → 계좌 0.
펀딩비 8시간 실이력(수량 × 가격 × 펀딩률, 롱이 내고 숏이 받는다) · 수수료 편도 0.1%.
신호는 4시간봉 종가까지의 정보로 정하고 그 종가에 바로 체결한 것으로 계산한다(호가 밀림 미반영).
데이터 = data_longshort/ 의 바이낸스 USDT-M 선물 4시간봉·펀딩비(ASOF 에서 잘라 얼림).
ASOF 를 바꾸면 영상과 숫자가 달라진다 — 영상은 2026-10-04 16:00(UTC) 봉까지다.
"""
import csv
import datetime
import os
import numpy as np
import strat_pool as P
HERE = os.path.dirname(os.path.abspath(__file__))
DATA = os.path.join(HERE, "data_longshort")
ASOF = "2026-10-04T16:00:00"
START = "2020-02-01"
WARM = 400
COST = 0.001
MAINT = 0.004
ASSETS = [("BTC", "BTCUSDT_4h.csv", "BTCUSDT_funding.csv"),
("ETH", "ETHUSDT_4h.csv", "ETHUSDT_funding.csv")]
def ts(x):
return datetime.datetime.fromisoformat(x if "T" in x else x + "T00:00:00")
def load(csvp, fp):
d, o, h, l, c = [], [], [], [], []
with open(os.path.join(DATA, csvp), encoding="utf-8") as f:
next(f)
for line in f:
p = line.rstrip("\n").split(",")
if ts(p[0]) > ts(ASOF):
break
d.append(p[0]); o.append(float(p[1])); h.append(float(p[2])); l.append(float(p[3])); c.append(float(p[4]))
S = dict(d=d, o=np.array(o), h=np.array(h), l=np.array(l), c=np.array(c), n=len(c))
S["ret"] = np.r_[0.0, S["c"][1:] / S["c"][:-1] - 1]
T = [ts(x) for x in d]
gaps = sum(1 for i in range(1, len(T)) if (T[i] - T[i - 1]).total_seconds() != 14400)
ev = {}
with open(os.path.join(DATA, fp), encoding="utf-8") as f:
for r in csv.DictReader(f):
ev[ts(r["date"]).isoformat()[:13]] = float(r["funding"])
fund = np.zeros(S["n"])
for i, t in enumerate(T): # 봉이 끝나는 시각의 정산
v = ev.get((t + datetime.timedelta(hours=4)).isoformat()[:13])
if v is not None:
fund[i] = v
a = max(WARM, int(np.searchsorted(np.asarray(d), START)))
return S, fund, a, gaps
def make_sig(S, fn, prm, filt_days):
base = fn(S["o"], S["h"], S["l"], S["c"], **prm)
if filt_days:
sma = P._sma(S["c"], filt_days * 6) # 4시간봉 6개 = 하루
f = np.zeros(S["n"])
m = ~np.isnan(sma)
f[m] = (S["c"][m] > sma[m]).astype(float)
base = base * f
return base
def daily_bear(S):
"""일봉 종가 < 200일 평균 — 하루가 끝난 뒤에야 안다(다음 날 봉부터)."""
days, last = [], {}
for i, x in enumerate(S["d"]):
k = x[:10]
if k not in last:
days.append(k)
last[k] = i
dc = np.array([S["c"][last[k]] for k in days])
cs = np.cumsum(np.r_[0.0, dc])
sma = np.full(len(dc), np.nan)
sma[199:] = (cs[200:] - cs[:-200]) / 200
idx = {k: j for j, k in enumerate(days)}
bear = np.zeros(S["n"], dtype=bool)
for i, x in enumerate(S["d"]):
j = idx[x[:10]] - 1
if j >= 199:
bear[i] = not (dc[j] > sma[j])
return bear
def run_held(S, fund, target, a):
"""수량 고정 1배 계좌. target[i] = i번째 봉 종가에 맞출 비중(-1~1). 반환: 곡선, 청산 시각, 펀딩 손익."""
c, h, l = S["c"], S["h"], S["l"]
eq, q, ent, base, cur, fsum = 1.0, 0.0, 0.0, 1.0, 0.0, 0.0
t0 = target[a - 1]
if t0 != 0.0:
eq -= COST * abs(t0) * eq
q, ent, base, cur = t0 * eq / c[a - 1], c[a - 1], eq, t0
curve = [eq]
for i in range(a, S["n"]):
if q != 0.0:
worst = h[i] if q < 0 else l[i]
if base + q * (worst - ent) <= MAINT * abs(q) * worst:
curve.extend([0.0] * (S["n"] - i))
return np.array(curve), S["d"][i], fsum
if fund[i] != 0.0:
pay = q * c[i] * fund[i]
base -= pay
fsum -= pay
eq = base + q * (c[i] - ent)
tgt = target[i]
if tgt != cur:
if q != 0.0:
eq -= COST * abs(q) * c[i]
q = 0.0
if tgt != 0.0:
eq -= COST * abs(tgt) * eq
q, ent = tgt * eq / c[i], c[i]
base, cur = eq, tgt
curve.append(eq)
return np.array(curve), None, fsum
def run_reb(S, fund, target, a):
"""민감도 — 봉마다 같은 비중으로 다시 맞추는 계좌(인버스 ETF 꼴)."""
pos = np.r_[0.0, target[:-1]]
g = (1 + pos * S["ret"] - pos * fund) * (1 - COST * np.abs(np.diff(np.r_[0.0, pos])))
return np.r_[1.0, np.cumprod(g[a:])]
def kpi(curve):
peak = np.maximum.accumulate(curve)
dd = curve / np.where(peak > 0, peak, 1) - 1
return float(curve[-1]), float(dd.min())
def 만원(x):
v = int(round(x * 1000))
return f"{v // 10000}억 {v % 10000:,}만원" if v >= 10000 else f"{v:,}만원"
def main():
rows, rep = [], None
for name, csvp, fp in ASSETS:
S, fund, a, gaps = load(csvp, fp)
bear = daily_bear(S)
print(f"[{name}] {S['d'][a - 1]} ~ {S['d'][-1]} (UTC) · 4시간봉 {S['n'] - a + 1:,}개 · 빠진 봉 {gaps}")
hold = float(S["c"][-1] / S["c"][a - 1])
fv = [v for v in fund[a:] if v != 0.0]
print(f" 그냥 들고만 있었다면(1배 롱 · 수수료·펀딩 없이) 1,000만원 → {만원(hold)}"
f" · 펀딩비가 플러스(롱이 내고 숏이 받음)였던 정산 {sum(v > 0 for v in fv) / len(fv) * 100:.0f}%")
cs, liq, _ = run_held(S, fund, -np.ones(S["n"]), a)
print(f" 1배 숏을 {S['d'][a - 1][:10]} 에 잡고 들고만 있었다면: 강제청산 {liq} "
f"(가격 {S['c'][a - 1]:,.0f} → 그날 고가 {S['h'][S['d'].index(liq)]:,.0f})")
for sname, (fn, grid, src) in P.POOL.items():
for prm in grid:
for filt in (0, 200):
sig = make_sig(S, fn, prm, filt)
tL = sig.copy()
tLS = 2.0 * sig - 1.0
tG = np.where(sig > 0, sig, np.where(bear, -(1.0 - sig), 0.0))
cL, _, _ = run_held(S, fund, tL, a)
cS, lqS, _ = run_held(S, fund, tLS, a)
cG, _, _ = run_held(S, fund, tG, a)
r = dict(asset=name, bot=sname, prm=prm, filt=filt,
L=kpi(cL), LS=kpi(cS), G=kpi(cG), liq=lqS,
LSr=run_reb(S, fund, tLS, a)[-1], Lr=run_reb(S, fund, tL, a)[-1])
rows.append(r)
if name == "BTC" and sname == "슈퍼트렌드" and prm == {"n": 10, "mult": 3.0} and filt == 0:
tS = -(1.0 - sig)
_, _, fS = run_held(S, fund, tS, a)
seg = t = 0
up = 0
pos = tLS[a - 1:]
cc = S["c"][a - 1:]
i = 0
while i < len(pos):
j = i
while j + 1 < len(pos) and pos[j + 1] == pos[i]:
j += 1
if pos[i] < 0:
seg += 1
up += int(cc[min(j + 1, len(cc) - 1)] > cc[i])
i = j + 1
fills_L = int(np.sum(np.diff(tL[a - 1:]) != 0))
rep = dict(r, segs=seg, up=up, fund_short=fS, fills_L=fills_L, fills_LS=2 * fills_L,
yrs=(ts(S["d"][-1]) - ts(S["d"][a - 1])).days / 365.25)
n = len(rows)
more = [r for r in rows if r["LS"][0] > r["L"][0]]
shal = [r for r in rows if r["LS"][1] > r["L"][1]]
both = [r for r in rows if r["LS"][0] > r["L"][0] and r["LS"][1] > r["L"][1]]
g_more = [r for r in rows if r["G"][0] > r["L"][0]]
g_both = [r for r in rows if r["G"][0] > r["L"][0] and r["G"][1] > r["L"][1]]
btc_gap = sorted(r["LS"][0] / r["L"][0] for r in rows if r["asset"] == "BTC")
rg = rep["LS"][0] / rep["L"][0]
print("\n① 대표 봇 — 트레이딩뷰 슈퍼트렌드 기본값(ATR 10 · 배수 3) · 비트코인 · 필터 없음 · 1,000만원")
print(f" 기간 {rep['yrs']:.2f}년 (6년 8개월)")
print(f" 롱만(팔라는 신호에 현금) : {만원(rep['L'][0])} · 고점 대비 가장 깊게 {rep['L'][1] * 100:.1f}%")
print(f" 숏도(팔라는 신호에 숏) : {만원(rep['LS'][0])} · 고점 대비 가장 깊게 {rep['LS'][1] * 100:.1f}%")
print(f" 팔라는 신호 {rep['segs']}번 중 다시 사라는 신호가 날 때 가격이 판 값보다 높았던 것 {rep['up']}번")
print(f" 숏 구간에서 받은 펀딩비(계좌 대비 누적) {rep['fund_short']:+.3f} — 받는 쪽이었다")
print(f" 체결 횟수 롱만 {rep['fills_L']}번 · 숏도 {rep['fills_LS']}번")
print(f" 숏으로 잃은 비율(숏도÷롱만) {rg:.3f} — 비트코인 222개 중 덜 잃은 쪽부터 "
f"{sum(x <= rg for x in btc_gap) / len(btc_gap) * 100:.0f}% 자리 (가운데 {np.median(btc_gap):.3f})")
all_gap = sorted(r["LS"][0] / r["L"][0] for r in rows)
print(f" 444개 전체로 세면 덜 잃은 쪽부터 {sum(x <= rg for x in all_gap) / len(all_gap) * 100:.0f}% 자리")
print(f"\n② 444개 전부 — 봇 13종 111설정 × 필터 2 × BTC·ETH = {n}개")
print(f" 숏도가 돈을 더 남긴 것 {len(more)}개 · 롱만이 더 남긴 것 {n - len(more)}개")
for r in more:
print(f" {r['asset']} {r['bot']} {r['prm']} 필터{r['filt']}: 롱만 {r['L'][0]:.2f}배/{r['L'][1] * 100:.1f}%"
f" · 숏도 {r['LS'][0]:.2f}배/{r['LS'][1] * 100:.1f}% ← 숏도가 더 깊게 떨어짐")
print(f" 숏도가 덜 떨어진 것 {len(shal)}개 · 돈도 더 남기고 덜 떨어진 것 {len(both)}개")
print(f" 숏도에서 강제청산된 설정 {sum(1 for r in rows if r['liq'])}개 "
f"(전부 {sorted(set(r['bot'] for r in rows if r['liq']))})")
print(f" 민감도 — 봉마다 다시 맞추는 계좌로 재면 숏도가 더 남긴 것 {sum(1 for r in rows if r['LSr'] > r['Lr'])}개")
print("\n③ 하락장일 때만 숏 — 일봉 종가가 200일 평균선 아래(전날 판정)인 날만 숏, 아니면 현금")
print(f" 롱만보다 돈을 더 남긴 것 {len(g_more)}개 · 돈도 더 남기고 덜 떨어진 것 {len(g_both)}개 "
f"(자산: {sorted(set(r['asset'] for r in g_both))})")
for nm in ("BTC", "ETH"):
k = sum(1 for r in rows if r["asset"] == nm)
print(f" {nm}: 돈을 더 남긴 것 {sum(1 for r in g_more if r['asset'] == nm)}개 / {k}개 · "
f"돈도 더 남기고 덜 떨어진 것 {sum(1 for r in g_both if r['asset'] == nm)}개")
if __name__ == "__main__":
main()
알고랩 Strategy Lab — 전략을 검증하고, 검증한 것만 자동매매로 옮깁니다. 떨어지는 차트를 보면서 숏을 참는 일은 사람보다 봇이 잘합니다. 알고랩 자동매매 살펴보기 · 전략 검증 전체 보기
과거 데이터를 돌려 본 결과이며 미래 수익을 보장하지 않습니다. 선물은 원금 이상 손실이 날 수 있습니다. 투자 판단과 책임은 본인에게 있습니다.