Strategy Lab · 손절·익절 규칙 실측

손절 −3%, 익절 +6% 를 102개 조합에 걸었습니다

전략은 한 줄도 고치지 않고 손절선과 익절선만 얹었습니다. 시장 17개 × 전략 6개 = 102개 조합에서 수익이 올라간 조합은 5개였습니다. 낙폭은 92개 조합에서 줄었습니다 — 방어는 진짜입니다. 다만 같은 방어를 비중 한 줄이 더 싸게 삽니다.

5 / 102수익이 오른 조합
92 / 102낙폭이 준 조합
1,426일신호는 켜졌는데 시장 밖
72 / 102비중 한 줄이 이긴 조합
재현 코드 + 시세 내려받기 102개 조합 전체 표 (CSV)

무엇을 어떻게 쟀나

비교는 항상 같은 조합 안에서만 합니다. 같은 시장, 같은 전략, 같은 구간에서 손절선과 익절선만 갈아끼웁니다. 다른 전략끼리 순위를 매기는 실험이 아닙니다.

· 전략 6종: 이동평균선 50/200 교차 · 돈치안 55 · 슈퍼트렌드 10/3 · UT봇 2.0 · 켈트너 돌파 20/1.5 · RSI 역추세 14

· 시장 17개: 미국 지수·3배 상품·개별 종목 / 코스피·국내 종목 / 비트코인·이더리움(일봉, 그리고 4시간봉은 업비트·바이낸스 둘 다)

· 크립토는 업비트와 바이낸스에서 각각 돌려 나쁜 쪽을 씁니다.

· 잣대는 둘입니다 — 얼마나 벌었는지(연복리), 그리고 중간에 얼마나 크게 잃었는지(최대낙폭). 손절은 낙폭을 낮추는 게 당연하므로 둘을 반드시 같이 봅니다.

· 노출은 돈이 실제로 시장에 걸려 있던 비율입니다. 이 값이 이 페이지의 핵심입니다.

· 비용은 편도 0.1%. 세금은 반영하지 않았습니다(손절·익절은 실현, 그냥 두는 쪽은 이연이라 세금을 넣으면 격차는 더 벌어집니다).

손절선을 좁힐수록 나빠집니다

102개 조합의 중앙값입니다. 손절선을 넓힐수록 성적이 올라가고, 가장 좋은 지점은 −30%가 아니라 손절선을 아예 지운 쪽이었습니다.

장치중앙 연복리중앙 최대낙폭 중앙 노출연복리 승낙폭 승 칼마 승
장치 없음15.5%-56.6%0.52
손절 −3%7.3%-46.5%0.3328 / 10286 / 10233 / 102
손절 −5%10.9%-47.1%0.4133 / 10284 / 10251 / 102
손절 −10%12.6%-51.6%0.4937 / 10263 / 10244 / 102
손절 −30%14.6%-56.1%0.529 / 10213 / 10211 / 102
손절 −3% + 익절 +6%1.4%-30.2%0.115 / 10292 / 10212 / 102

승·패는 같은 조합의 장치 없는 판과 견준 것입니다. 낙폭은 거의 모든 조합에서 이깁니다 — 손절이 방어를 못 하는 게 아닙니다.

천만원을 넣었다면

미국 대표 지수에 가장 널리 알려진 이동평균선 50/200 교차를 붙인 한 조합입니다 (2006-01-12 ~ 2026-09-04 · 20.6년). 가장 극적인 조합이 아니라 가장 교과서적인 조합을 골랐습니다.

규칙20.6년 뒤연복리 최대낙폭노출
신호대로만 (손절 없음)59,124,731원9.0%-33.7%0.79
손절 −3%23,227,478원4.2%-27.6%0.51
손절 −3% + 익절 +6%11,948,969원0.9%-13.1%0.07
손절선 지우고 비중 65% 한 줄33,281,812원6.0%-23.0%0.79

범인은 손절이 아니었습니다

위 조합에서 손절로 잘려 나간 거래는 7건뿐입니다. 그런데 잘린 뒤가 문제였습니다.

· 신호대로만 갔다면 시장에 들어가 있었을 날: 4,088일

· 손절선을 걸었을 때 실제로 들어가 있던 날: 2,662일

· 차이 1,426일 — 신호는 여전히 켜져 있는데 계좌만 밖에 나와 있던 날입니다.

· 손절이 한 일은 손실을 자르는 것이 아니라 시장 밖에 앉혀두는 것이었습니다.

· 다만 이건 이 조합에서 관찰된 일입니다. 손절이 언제나 이렇게 작동한다는 뜻은 아닙니다 — 102개 조합에서 같은 방향이 반복됐다는 것까지가 저희가 말할 수 있는 전부입니다.

처방 — 손절선 대신 비중 한 줄

손절을 건 계좌가 실제로 굴린 돈은 평균 65%였습니다. 나머지는 현금으로 놀고 있었습니다. 그러면 손절선을 지우고 처음부터 65%만 넣으면 어떻게 될까요? 규칙은 하나 줄어듭니다.

· 102개 조합 중앙: 연복리 9.1% · 최대낙폭 -37.7%

· 손절 −3% 와 견주면 — 칼마 승 72 / 102 · 연복리 승 59 / 102

· 위 천만원 조합에서도 손절을 건 계좌보다 더 벌었고, 아무것도 안 건 계좌보다 덜 잃었습니다.

자가반박 — 종가로만 재서 그런 것 아닌가

본 계산은 종가 마감 기준입니다. 봉 안에서 저가가 손절선을 스쳐도 종가가 위면 살려둡니다 — 이건 손절에 유리한 가정입니다. 그래서 봉 안에서 저가가 손절선을 스치는 순간 자르는 모델로 다시 쟀습니다. 중앙 연복리 1.1% · 연복리 승 9 / 102 — 결론은 같고, 오히려 조금 더 나빴습니다.

한계 — 이건 예측이 아닙니다

· 과거 데이터 백테스트입니다. 미래 수익을 보장하지 않습니다.

· 비용 편도 0.1% · 세금 미반영 · 호가 밀림(슬리피지) 미반영.

· 전부 매수만입니다. 공매도는 넣지 않았습니다.

· 지금 상장돼 있는 시장 17개만 썼습니다. 사라진 종목은 성적표에 없습니다.

· 전략 파라미터는 대표값 고정입니다. 손절 장치의 부호를 보는 실험이지, 전략의 순위를 매기는 실험이 아닙니다.

· 손절이 이긴 조합도 실제로 있습니다(위 표의 승 열). 이 페이지의 주장은 "손절이 항상 나쁘다"가 아니라 "같은 방어를 더 싸게 사는 방법이 있다"입니다.

재현 코드 — 그대로 돌아갑니다

아래 코드는 이 페이지의 모든 숫자를 처음부터 다시 만듭니다. 영상과 같은 숫자가 나오도록 그 시점의 시세를 그대로 동봉했습니다(data_asof/). 4시간봉은 마지막 봉이 닫히기 전에 받으면 금액이 1%쯤 움직입니다. 손절선 숫자만 바꿔가며 직접 돌려보십시오.

stoploss_repro.zip 내려받기 (2.5MB) run_stop.py 만 보기

# -*- coding: utf-8 -*-
r"""손절선·익절선 전수 재현 — 영상 「교과서대로 손절했는데, 왜 수익만 사라졌을까?」

이 파일 하나로 영상에 나온 숫자를 전부 다시 만듭니다.

  python run_stop.py

같이 들어 있는 것
  strat_pool.py / overlays.py / runlib.py / sweep.py   전략·오버레이·러너
  data_asof/                                           2026-09-08 시점으로 얼린 시세

★ 왜 시세를 같이 넣었나
  4시간봉은 마지막 봉이 아직 닫히지 않은 채로 받아지면 5년 경로 금액이 1퍼센트쯤 어긋납니다.
  그래서 영상을 만든 시점의 캔들을 그대로 얼려 동봉했습니다. 최신 데이터로 보고 싶으시면
  showcase 캐시를 직접 갱신해 SHOW 경로만 바꾸시면 됩니다(숫자는 당연히 달라집니다).

무엇을 재는가
  같은 신호·같은 파라미터·같은 구간에서 **손절선과 익절선만** 갈아끼웁니다.
  비교는 항상 '같은 조합 안에서' 합니다. 다른 전략끼리 순위를 매기는 실험이 아닙니다.

잣대
  · 연복리(CAGR)  — 얼마나 벌었나
  · 최대낙폭(MDD) — 중간에 얼마나 크게 잃었나
  · 칼마 = 연복리 ÷ 최대낙폭 — 손절은 낙폭을 낮추는 게 당연하므로 둘을 같이 봅니다
  · 평균 노출     — 돈이 실제로 시장에 걸려 있던 비율

한계 (영상과 같은 표기)
  · 매매 비용 편도 0.1퍼센트. 세금 미반영(손절·익절은 실현이고 그냥 두는 쪽은 이연이라
    세금을 넣으면 격차는 더 벌어집니다 — 손절에 유리한 방향의 생략이 아닙니다)
  · 손절 판정은 종가 마감 기준. 봉내 저가 터치 모델도 같이 잽니다(자가반박)
  · 호가 밀림(슬리피지) 미모형 · 전부 매수만(공매도 없음)
  · 지금 상장돼 있는 17개 시장만. 사라진 종목은 들어 있지 않습니다
"""
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__))
sys.path.insert(0, HERE)

import strat_pool as P                     # noqa: E402
import overlays as OV                      # noqa: E402
from runlib import series_data, growth, stats  # noqa: E402

ASOF = datetime.date(2026, 9, 8)           # 영상을 만든 날. 얼린 시세가 여기까지입니다
SHOW = os.path.join(HERE, "data_asof")     # 최신으로 보려면 이 경로를 캐시 폴더로 바꾸세요
COST = 0.001                               # 편도 0.1퍼센트
SEED = 10_000_000                          # 천만원

SERIES = [
    ("비트코인(업비트)",   "sc_btc_4h.csv",     "4h", 400),
    ("비트코인(바이낸스)",  "sc_btcusdt_4h.csv", "4h", 400),
    ("이더리움(업비트)",   "sc_eth_4h.csv",     "4h", 400),
    ("이더리움(바이낸스)",  "sc_ethusdt_4h.csv", "4h", 400),
    ("비트코인(일봉)",     "sc_btc_1d.csv",     "1d", 260),
    ("이더리움(일봉)",     "sc_eth_1d.csv",     "1d", 260),
    ("SPY",              "sc_spy_1d.csv",     "1d", 260),
    ("QQQ",              "sc_qqq_1d.csv",     "1d", 260),
    ("TQQQ(3배)",        "sc_tqqq_1d.csv",    "1d", 260),
    ("SOXL(3배)",        "sc_soxl_1d.csv",    "1d", 260),
    ("엔비디아",          "sc_nvda_1d.csv",    "1d", 260),
    ("테슬라",            "sc_tsla_1d.csv",    "1d", 260),
    ("애플",              "sc_aapl_1d.csv",    "1d", 260),
    ("마이크로소프트",      "sc_msft_1d.csv",    "1d", 260),
    ("삼성전자",          "sc_samsung_1d.csv", "1d", 260),
    ("SK하이닉스",        "sc_hynix_1d.csv",   "1d", 260),
    ("코스피",            "sc_kospi_1d.csv",   "1d", 260),
]
STRATS = [
    ("이평크로스50/200", P.ma_cross,     dict(fast=50, slow=200)),
    ("돈치안55",        P.donchian,      dict(n=55)),
    ("슈퍼트렌드10/3",   P.supertrend,    dict(n=10, mult=3.0)),
    ("UT봇2.0",         P.ut_bot,        dict(key=2.0, atr_n=10)),
    ("켈트너돌파20/1.5", P.keltner_break, dict(n=20, k=1.5)),
    ("RSI역추세",       P.rsi_meanrev,   dict(n=14, lo=30, hi=70)),
]
SL_LADDER = [0.03, 0.05, 0.10, 0.30]
TP_MAIN = 0.06


def cal(cg, m):
    return cg / abs(m) if m and m < 0 else float("nan")


def bracket(c, sig, sl, tp, low=None):
    """진입 종가 대비 -sl 손절 / +tp 익절. low 를 주면 봉내 저가 터치 판정."""
    st = {}

    def entry(t):
        st["e"] = c[t]

    def hold(t):
        e = st["e"]
        if low is not None and low[t] <= e * (1 - sl):
            return False, 1.0
        if c[t] / e - 1 <= -sl:
            return False, 1.0
        if tp is not None and c[t] / e - 1 >= tp:
            return False, 1.0
        return True, 1.0

    return OV._iterate(sig, entry, hold)


def med(xs):
    v = [float(x) for x in xs if x == x]
    return float(np.median(v)) if v else float("nan")


def run():
    rows = []
    for label, fname, tf, warm in SERIES:
        S = series_data(os.path.join(SHOW, fname), tf, warm=warm)
        for sname, fn, prm in STRATS:
            base = np.nan_to_num(fn(S["o"], S["h"], S["l"], S["c"], **prm), nan=0.0)
            pb, gb = growth(S, base, COST)
            rb = stats(S, gb)
            eb = float((pb[warm:] > 0).mean())
            rec = dict(자산=label, 전략=sname, 시작=S["d"][warm][:10], 끝=S["d"][-1][:10],
                       기준_cagr=rb["cagr"], 기준_mdd=rb["mdd"],
                       기준_cal=cal(rb["cagr"], rb["mdd"]), 기준_노출=eb)
            for sl in SL_LADDER:
                sig = bracket(S["c"], base, sl, None)
                ps, gs = growth(S, sig, COST)
                r = stats(S, gs)
                rec["sl%d" % int(sl * 100)] = dict(
                    cagr=r["cagr"], mdd=r["mdd"], calmar=cal(r["cagr"], r["mdd"]),
                    노출=float((ps[warm:] > 0).mean()))
            sig = bracket(S["c"], base, 0.03, TP_MAIN)
            ps, gs = growth(S, sig, COST)
            r = stats(S, gs)
            rec["격언"] = dict(cagr=r["cagr"], mdd=r["mdd"], calmar=cal(r["cagr"], r["mdd"]),
                              노출=float((ps[warm:] > 0).mean()))
            _, g_ib = growth(S, bracket(S["c"], base, 0.03, TP_MAIN, low=S["l"]), COST)
            r_ib = stats(S, g_ib)
            rec["격언_봉내"] = dict(cagr=r_ib["cagr"])
            e3 = rec["sl3"]["노출"]
            w = min(e3 / eb, 1.0) if eb > 0 else 0.0
            pw, gw = growth(S, base * w, COST)
            rw = stats(S, gw)
            rec["비중"] = dict(cagr=rw["cagr"], mdd=rw["mdd"],
                              calmar=cal(rw["cagr"], rw["mdd"]), w=w)
            rows.append(rec)
    return rows


def 대표칸():
    """영상에 나온 천만원 경로 — 미국 대표 지수 + 이동평균선 50/200 교차."""
    S = series_data(os.path.join(SHOW, "sc_spy_1d.csv"), "1d", warm=260)
    base = np.nan_to_num(P.ma_cross(S["o"], S["h"], S["l"], S["c"], fast=50, slow=200), nan=0.0)
    pb, gb = growth(S, base, COST)
    eb = float((pb[260:] > 0).mean())
    sl = bracket(S["c"], base, 0.03, None)
    psl, gsl = growth(S, sl, COST)
    esl = float((psl[260:] > 0).mean())
    tp = bracket(S["c"], base, 0.03, TP_MAIN)
    ptp, gtp = growth(S, tp, COST)
    w = min(esl / eb, 1.0)
    pw, gw = growth(S, base * w, COST)
    out = []
    for name, pos, g in (("신호대로", pb, gb), ("손절 -3%", psl, gsl),
                         ("손절 -3% + 익절 +6%", ptp, gtp), ("비중 %.0f%% 한 줄" % (w * 100), pw, gw)):
        eq = np.cumprod(g[260:])
        r = stats(S, g)
        out.append((name, SEED * float(eq[-1]), r["cagr"], r["mdd"],
                    float((pos[260:] > 0).mean())))
    inb, ins = pb[260:] > 0, psl[260:] > 0
    return S["d"][260][:10], S["d"][-1][:10], out, int(inb.sum()), int(ins.sum())


if __name__ == "__main__":
    print("기준일 %s · 매매 비용 편도 %.1f%% · 세금 미반영\n" % (ASOF, COST * 100))

    a, b, rep, hold_base, hold_sl = 대표칸()
    yrs = (datetime.date.fromisoformat(b) - datetime.date.fromisoformat(a)).days / 365.25
    print("■ 대표 칸 — 미국 대표 지수 + 이동평균선 50/200 교차")
    print("   %s ~ %s (%.1f년) · 천만원으로 시작" % (a, b, yrs))
    for name, money, cg, md, ex in rep:
        print("   %-20s %14s원 · 연복리 %5.1f%% · 최대낙폭 %6.1f%% · 노출 %.2f"
              % (name, format(int(round(money)), ","), cg * 100, md * 100, ex))
    print("   시장에 들어가 있던 날: 신호대로 %s일 → 손절 -3%% %s일 (차이 %s일)\n"
          % (format(hold_base, ","), format(hold_sl, ","), format(hold_base - hold_sl, ",")))

    rows = run()
    n = len(rows)
    print("■ 전 조합 %d개 (시장 17 × 전략 6) — 같은 조합 안에서만 비교" % n)
    print("   %-22s %9s %9s %7s %9s %9s %10s"
          % ("장치", "중앙 연복리", "중앙 낙폭", "노출", "연복리 승", "낙폭 승", "칼마 승"))

    def line(name, get):
        cg = med([get(r)["cagr"] for r in rows])
        md = med([get(r)["mdd"] for r in rows])
        ex = med([get(r).get("노출", float("nan")) for r in rows])
        w1 = sum(1 for r in rows if get(r)["cagr"] > r["기준_cagr"])
        w2 = sum(1 for r in rows if get(r)["mdd"] > r["기준_mdd"])
        w3 = sum(1 for r in rows if get(r)["calmar"] > r["기준_cal"])
        print("   %-22s %8.1f%% %8.1f%% %7.2f %6d/%-3d %6d/%-3d %7d/%-3d"
              % (name, cg * 100, md * 100, ex, w1, n, w2, n, w3, n))

    print("   %-22s %8.1f%% %8.1f%% %7.2f"
          % ("(기준선 · 장치 없음)", med([r["기준_cagr"] for r in rows]) * 100,
             med([r["기준_mdd"] for r in rows]) * 100, med([r["기준_노출"] for r in rows])))
    for sl in SL_LADDER:
        line("손절 -%d%%" % int(sl * 100), (lambda k: (lambda r: r[k]))("sl%d" % int(sl * 100)))
    line("손절 -3% + 익절 +6%", lambda r: r["격언"])

    ib = med([r["격언_봉내"]["cagr"] for r in rows])
    ibw = sum(1 for r in rows if r["격언_봉내"]["cagr"] > r["기준_cagr"])
    print("\n   [자가반박] 봉내 저가 터치로 손절해도  중앙 연복리 %.1f%% · 연복리 승 %d/%d"
          % (ib * 100, ibw, n))

    w_cal = sum(1 for r in rows if r["비중"]["calmar"] > r["sl3"]["calmar"])
    w_cg = sum(1 for r in rows if r["비중"]["cagr"] > r["sl3"]["cagr"])
    print("\n■ 처방 — 손절선 대신 '같은 노출을 만드는 비중 한 줄'")
    print("   중앙 연복리 %.1f%% · 중앙 낙폭 %.1f%%"
          % (med([r["비중"]["cagr"] for r in rows]) * 100,
             med([r["비중"]["mdd"] for r in rows]) * 100))
    print("   손절 -3%% 대비 — 칼마 승 %d/%d · 연복리 승 %d/%d" % (w_cal, n, w_cg, n))

    win = sum(1 for r in rows if r["격언"]["cagr"] > r["기준_cagr"])
    print("\n한 줄 요약: 손절 -3%% + 익절 +6%% 를 얹어 수익이 올라간 조합은 %d개 / %d개." % (win, n))
    print("           덜 잃고 싶다면 손절선이 아니라 비중을 줄이는 쪽이 쌉니다.")

그래서, 이 규칙을 사람이 지킬 수 있을까

손절선을 지우는 건 코드 한 줄이면 됩니다. 어려운 건 그다음입니다. 계좌가 −20%일 때 "이번엔 진짜 다른 것 같은데?" 하고 한 번 손을 대는 순간, 20년의 계산이 지워집니다. 손절선은 무서울 때 만드는 규칙이고, 비중은 무섭지 않을 때 미리 정해두는 크기입니다. 그 크기를 매일 흔들리지 않고 지키는 일은 사람보다 기계가 잘합니다.

내 전략으로 이 검증 맡기기 다른 검증 보기
Strategy Lab · 이 검증 더 보기
낙폭 −53% 전략 — 장치 22개 중 살린 건 1개낙폭 장치를 같은 방식으로 전수한 편 같은 전략을 29개 자산에 붙였더니이 편에 쓴 시장 캐시의 출처 전략 검증 전체 보기 →

본 페이지는 과거 데이터에 근거한 백테스트 결과이며 투자 자문이나 수익 보장이 아닙니다. 암호화폐·주식 투자는 원금 손실 위험이 있습니다. 투자 판단과 그 결과는 투자자 본인에게 귀속됩니다.