AlgoLab Blog · 퀀트·백테스트 검증 · 2026

백테스트 손절·익절 같은 날 — 거래 41%가 갈렸다

퀀트 · 백테스트 2026-10-12 · 약 8분 읽기 · 알고랩 AlgoLab
한 줄 요약

일봉 데이터에는 하루 안에서 고가와 저가 중 무엇이 먼저 찍혔는지가 없습니다. 그래서 손절가와 익절가가 같은 날 둘 다 닿으면 백테스트는 어느 쪽으로 처리할지 가정해야 하고, 그 가정이 결과를 바꿉니다. 코스닥 고변동 5종목에 손절 1%·익절 2% 규칙을 걸었더니 거래 461건 중 189건(41.0%)이 그런 날이었고, 거래당 평균 수익률이 손절 먼저 가정 −0.678%, 익절 먼저 가정 +0.552%로 부호까지 뒤집혔습니다. 대형주 10종목·손절 5%·익절 8%에서는 0건이었습니다. backtesting.py 0.6.6과 backtrader 1.9.78은 시험에서 둘 다 손절 먼저로 처리했습니다. (2016-01-04~2026-09-30, 왕복 비용 0.2%, 설명용 전략, 과거≠미래)

목차

  1. 무엇이 문제인가 — 봉 하나에 경로가 두 개
  2. 라이브러리는 어떻게 처리하나 — 직접 시험
  3. 실측 설계
  4. 결과 — 언제 문제가 되고 언제 안 되나
  5. 같은 날보다 자주 생기는 것 — 갭
  6. 분봉으로 그날만 다시 판정하기
  7. 제작 의뢰 전에 확인할 것
  8. 재현 코드
  9. 자주 묻는 질문

1. 무엇이 문제인가 — 봉 하나에 경로가 두 개

오늘 시가 100원에 샀고 손절가 97원, 익절가 105원을 걸었다고 하겠습니다. 그날 일봉이 시가 100 · 고가 106 · 저가 94 · 종가 104로 끝났습니다. 고가 106은 익절가보다 높고 저가 94는 손절가보다 낮으니 둘 다 닿았습니다. 실제로는 둘 중 먼저 닿은 쪽에서 주문이 체결되고 다른 쪽은 취소됩니다(OCO). 문제는 일봉 Open·High·Low·Close 네 숫자로는 그 순서를 알 수 없다는 점입니다.

TP 105 SL 97 진입 100 일봉 1개 경로 A: 시가→저가→고가→종가 97에서 손절, −3% 경로 B: 시가→고가→저가→종가 105에서 익절, +5%
같은 OHLC(100·106·94·104)가 두 경로 모두에서 나온다 — 일봉만으로는 결과가 −3%인지 +5%인지 정할 수 없다

한 거래에서 −3%와 +5%의 차이는 8%p입니다. 이런 날이 드물면 무시해도 되지만, 많으면 백테스트 결과가 전략이 아니라 가정을 측정하게 됩니다. 흔히 쓰는 가정은 네 가지입니다.

가정처리성격
손절 먼저 (pess)같은 날이면 무조건 SL 체결보수적. 결과를 부풀리지 않는다
익절 먼저 (opt)같은 날이면 무조건 TP 체결낙관적. 판매용 성과표에서 조심할 쪽
양음봉 휴리스틱 (candle)양봉(Close >= Open)이면 경로 A, 음봉이면 경로 B그럴듯하지만 근거는 약하다
분봉 판정그날 1분봉으로 실제 순서 확인가장 정확. 데이터 확보가 일

2. 라이브러리는 어떻게 처리하나 — 직접 시험

파이썬 백테스트 라이브러리가 이 상황을 어떻게 처리하는지는 문서에 잘 나오지 않습니다. 그래서 2026-10-12에 직접 시험했습니다. 7거래일짜리 가짜 데이터를 만들고, 4번째 봉만 시가 100 · 고가 106 · 저가 94 · 종가 104로 둔 뒤 전날 시가 100에 사서 손절 97·익절 105를 걸었습니다.

import pandas as pd, backtrader as bt
from backtesting import Backtest, Strategy

idx = pd.date_range("2026-01-05", periods=7, freq="B")
df = pd.DataFrame({"Open":[100]*7, "High":[101,101,101,106,101,101,101],
                   "Low":[99,99,99,94,99,99,99], "Close":[100,100,100,104,100,100,100],
                   "Volume":[1e6]*7}, index=idx)

class S(Strategy):                      # backtesting.py
    def init(self): pass
    def next(self):
        if len(self.data) == 2 and not self.position:
            self.buy(size=100, sl=97, tp=105)

class B(bt.Strategy):                   # backtrader
    def next(self):
        if len(self) == 2 and not self.position:
            self.buy_bracket(size=100, exectype=bt.Order.Market,
                             stopprice=97, limitprice=105)

실행 결과(Python 3.10.4, pandas 2.2.3):

backtesting 0.6.6 entry 2026-01-07 100.0 exit 2026-01-08 97.0
backtrader 1.9.78.123 BUY 2026-01-07 100.0
backtrader 1.9.78.123 SELL 2026-01-08 97.0

두 라이브러리 모두 손절가 97에 청산했습니다. 종가 104로 끝난 양봉이었는데도 그렇습니다. 즉 기본 동작은 보수적인 쪽입니다. 같은 방식으로 시가가 95(손절가 아래)에서 열리는 봉과 107(익절가 위)에서 열리는 봉도 넣어 봤는데, 둘 다 시가 95, 시가 107에 청산했습니다. 갭은 스탑 가격이 아니라 시가로 처리한다는 뜻입니다.

backtesting.py는 소스에 관련 경고도 들어 있습니다. 진입 주문이 지정가·스탑 주문이고 같은 봉에서 손절·익절까지 닿으면 이런 경고를 내고 손절·익절을 다음 봉으로 미룹니다.

UserWarning: A contingent SL/TP order would execute in the same bar its parent
stop/limit order was turned into a trade. Since we can't assert the precise
intra-candle price movement, the affected SL/TP order will instead be executed
on the next (matching) price/bar, making the result (of this trade) somewhat dubious.
See https://github.com/kernc/backtesting.py/issues/119

"라이브러리가 보수적이니 괜찮다"로 끝나지 않는 이유 — 직접 짠 pandas 루프, 엑셀, 트레이딩뷰 스크립트, 판매자가 만든 성과표는 이 처리를 어떻게 했는지 알 수 없습니다. if high >= tp: ... elif low <= sl:처럼 익절 검사를 먼저 쓰면 그 자체로 "익절 먼저" 가정이 됩니다. 그리고 라이브러리 기본 동작도 버전에 따라 바뀔 수 있으니, 위 시험을 쓰는 버전에서 한 번 돌려 보는 것이 가장 확실합니다. 라이브러리별 특징은 파이썬 백테스트 라이브러리 5종 비교에 정리했습니다.

3. 실측 설계

그럼 실제 데이터에서 이런 날이 얼마나 나올까요. 전략은 일부러 단순하게 잡았습니다. 수익을 내는 전략을 찾는 실험이 아니라 가정 하나가 결과를 얼마나 흔드는지 재는 실험입니다.

항목설정
데이터FinanceDataReader fdr.DataReader(code, "2016-01-04") 일봉, ASOF = "2026-09-30" 고정
대형 10069500(KODEX 200) · 005930 · 000660 · 005380 · 035420 · 051910 · 006400 · 035720 · 068270 · 105560
코스닥 고변동 5247540(2019-03-05~) · 086520 · 293490(2020-09-10~) · 196170 · 028300
신호종가 > 직전 20거래일 고가의 최고값 → 다음 거래일 시가 진입
청산손절 SL = 진입가 × (1 − s), 익절 TP = 진입가 × (1 + p), 최대 10거래일 후 종가
폭 (s / p)1%/2% · 2%/3% · 3%/5% · 5%/8%
비용왕복 0.2% 근사 (거래비용 반영 글 기준)
환경Python 3.10.4 · pandas 2.2.3 · FinanceDataReader 0.9.202

종목은 "자주 거래되는 대형주"와 "하루 변동폭이 큰 코스닥 종목" 두 묶음으로 나눴습니다. 종목 선택은 설명용이며 추천이 아닙니다. 진입 당일도 시가 이후 저가·고가가 손절·익절에 닿을 수 있으므로 진입일부터 검사합니다.

4. 결과 — 언제 문제가 되고 언제 안 되나

아래 숫자는 같은 거래 목록에서 같은 날 둘 다 닿은 거래만 처리 방식을 바꿔 계산한 거래당 평균 수익률입니다. 나머지 거래는 네 열 모두 똑같습니다.

묶음SL / TP거래같은 날손절 먼저양음봉익절 먼저
대형 101% / 2%1,211104 (8.6%)−0.343%−0.259%−0.085%
대형 102% / 3%1,06320 (1.9%)−0.255%−0.222%−0.161%
대형 103% / 5%9263 (0.3%)−0.075%−0.057%−0.049%
대형 105% / 8%7870 (0.0%)0.410%0.410%0.410%
코스닥 51% / 2%461189 (41.0%)−0.678%−0.176%+0.552%
코스닥 52% / 3%45487 (19.2%)−0.830%−0.389%+0.129%
코스닥 53% / 5%41733 (7.9%)−0.601%−0.314%+0.032%
코스닥 55% / 8%3627 (1.9%)−0.124%+0.055%+0.127%

세 가지가 읽힙니다.

  1. 대형주에 넓은 폭이면 사실상 문제가 없다. 손절 3%·익절 5% 이상에서 같은 날 비율은 0.3% 이하이고, 세 가정의 차이는 거래당 0.03%p 안쪽입니다. 하루 변동폭이 손절 폭과 익절 폭의 합(8%)을 넘는 날이 드물기 때문입니다.
  2. 폭이 좁고 종목 변동성이 크면 결론이 바뀐다. 코스닥 5종목·1%/2%는 거래 10건 중 4건이 판정 불가였고, 손절 먼저면 "거래당 −0.68%로 버릴 전략", 익절 먼저면 "+0.55%로 해볼 만한 전략"이 됩니다. 같은 코드, 같은 데이터에서 if 문 순서 하나로 나온 차이입니다.
  3. 양음봉 휴리스틱은 중간값을 줄 뿐 답이 아니다. 1%/2%에서 −0.176%로 두 극단의 가운데쯤에 놓였지만, 이게 실제에 가깝다는 근거는 없습니다. 장 초반에 급등했다가 밀려 양봉으로 끝나는 날도 흔합니다.

판단 기준 — 같은 날 비율이 몇 %를 넘으면 위험하다는 정해진 선은 없습니다. 실무에서는 손절 먼저와 익절 먼저 두 결과가 서로 다른 결론(채택/기각)을 내는지를 봅니다. 결론이 같으면 일봉으로 충분하고, 결론이 갈리면 그 전략은 일봉으로 검증되지 않은 것입니다. 이 표에서는 코스닥 5종목의 1%/2%·2%/3%·3%/5%가 그 경우였습니다.

5. 같은 날보다 자주 생기는 것 — 갭

대형주 3%/5%에서 같은 날은 926건 중 3건뿐이었지만, 시가가 이미 손절가 아래나 익절가 위에서 열린 거래는 125건(13.5%)이었습니다. 이런 날 체결가를 손절가 97로 잡으면 안 되고 시가로 잡아야 합니다. 2장 시험처럼 backtesting.py와 backtrader는 시가로 처리합니다.

직접 짠 코드에서 갭을 무시하고 손절·익절 가격 그대로 체결했다고 계산해 보면(같은 날은 손절 먼저), 대형 10 · 3%/5%에서 거래당 평균이 −0.075% → −0.113%로 달라졌습니다. 이 전략은 돌파 진입이라 위쪽 갭(익절가보다 높게 체결될 기회)이 많아서, 갭을 무시하면 오히려 결과가 나빠지는 쪽으로 틀렸습니다. 손절 위주의 역추세 전략이라면 반대로 결과가 부풀려집니다. 방향이 전략마다 다르다는 것이 갭 처리를 꼭 확인해야 하는 이유입니다.

실전 봇에서도 같은 문제가 있습니다. 장 시작 전에 손절 주문을 어떻게 걸어 두느냐에 따라 시가 단일가에서 체결되는 가격이 달라집니다. 이 부분은 백테스트와 실전 수익률 차이에서 체결 관점으로 다뤘습니다.

6. 분봉으로 그날만 다시 판정하기

결론이 갈리는 전략이라면 전체를 분봉으로 다시 돌릴 필요는 없습니다. 같은 날 둘 다 닿은 거래의 그날 분봉만 받아서 무엇이 먼저였는지 확인하면 됩니다. 분봉 데이터는 증권사 API로 받습니다.

분봉을 pandas DataFrame으로 받았다면 판정 함수는 짧습니다. 열 이름은 데이터 소스에 맞게 바꿔 쓰면 됩니다.

import pandas as pd

def first_touch(minute_df: pd.DataFrame, sl: float, tp: float) -> str:
    """그날 1분봉으로 손절·익절 중 먼저 닿은 쪽을 판정한다.
    minute_df: 시간순 정렬, 열 High / Low"""
    for ts, row in minute_df.iterrows():
        hit_sl, hit_tp = row["Low"] <= sl, row["High"] >= tp
        if hit_sl and hit_tp:
            return "ambiguous_1m"      # 1분봉 안에서도 둘 다 — 손절로 처리
        if hit_sl:
            return "sl"
        if hit_tp:
            return "tp"
    return "none"                      # 일봉과 분봉이 안 맞으면 데이터부터 의심

두 가지를 조심합니다. 첫째, 1분봉 안에서도 둘 다 닿을 수 있습니다(급등락 종목). 그때는 손절로 처리하거나 틱 데이터가 필요합니다. 둘째, 일봉의 고가·저가와 분봉을 모은 고가·저가가 다르면(시간외·단일가 처리, 수정주가 차이) 판정보다 데이터 대조가 먼저입니다. KIS 분봉 API는 보관 기간이 정해져 있어 몇 년 전 거래까지 거슬러 판정하기는 어렵다는 점도 계획에 넣어야 합니다.

7. 제작 의뢰 전에 확인할 것

손절·익절이 있는 전략의 백테스트를 받았다면, 또는 판매 페이지의 성과표를 보고 있다면 이 네 가지를 물어보십시오.

  1. 몇 분봉으로 돌렸나 — 일봉이라면 같은 날 처리 규칙이 반드시 있어야 합니다.
  2. 같은 날 둘 다 닿은 거래가 몇 건인가 — 이 숫자를 바로 답하지 못하면 확인하지 않은 것입니다.
  3. 손절 먼저로 다시 돌린 결과가 있나 — 4장처럼 두 결과를 나란히 보여 줄 수 있어야 합니다.
  4. 갭 체결가를 시가로 잡았나 — 손절가 그대로 잡았다면 결과가 어느 쪽으로든 틀어져 있습니다.

이 점검은 미래참조 편향 점검과 짝을 이룹니다. 미래참조가 "아직 모르는 값을 썼나"를 묻는다면, 같은 날 문제는 "모르는 순서를 유리하게 정했나"를 묻습니다. 백테스트 도구를 고르는 단계에서 이런 함정을 묶어 본 글은 백테스팅 프로그램·사이트 5갈래 비교이고, 전체 흐름은 자동매매 백테스트 완전 가이드에 있습니다.

8. 재현 코드

4장 표 전체를 만드는 스크립트는 intrabar_sl_tp_ambiguity.py로 받을 수 있습니다. ASOF를 2026-09-30으로 고정해 두었고, 빈 폴더에서 두 번 처음부터 실행해 같은 출력이 나오는 것을 확인했습니다. 최신 데이터로 보려면 ASOF = None으로 바꾸면 되지만 숫자는 달라집니다. 핵심은 같은 날을 따로 표시해 두고 나중에 처리 방식만 바꾸는 구조입니다.

for j in range(e, min(e + MAX_HOLD, n)):
    if j > e and O[j] <= sl:                    # 갭 하락 → 시가 체결
        res = dict(kind="gap_sl", x=O[j], j=j); break
    if j > e and O[j] >= tp:                    # 갭 상승 → 시가 체결
        res = dict(kind="gap_tp", x=O[j], j=j); break
    hit_sl, hit_tp = L[j] <= sl, H[j] >= tp
    if hit_sl and hit_tp:                        # 같은 날 — 판정 보류
        res = dict(kind="both", j=j, bull=C[j] >= O[j]); break
    if hit_sl:
        res = dict(kind="sl", x=sl, j=j); break
    if hit_tp:
        res = dict(kind="tp", x=tp, j=j); break

실행 결과 일부(코스닥 고변동 5):

[코스닥 고변동 5] 247540, 086520, 293490, 196170, 028300
  SL/TP    거래   같은날     비율   갭체결     손절먼저      양음봉     익절먼저      갭무시
  1%/2%   461   189   41.0%     2  -0.678%  -0.176%   0.552%  -0.692%
  2%/3%   454    87   19.2%    16  -0.830%  -0.389%   0.129%  -0.834%
  3%/5%   417    33    7.9%    30  -0.601%  -0.314%   0.032%  -0.603%
  5%/8%   362     7    1.9%    32  -0.124%   0.055%   0.127%  -0.151%

한계 — FinanceDataReader 일봉(수정주가 기준) 15종목·전략 1개·특정 기간의 실험입니다. 247540·293490은 상장 이후 구간만 들어갔습니다. 비용은 왕복 0.2% 근사이고 호가 단위·슬리피지·시가 단일가 체결 불확실성은 반영하지 않았습니다. 라이브러리 시험은 명시한 버전에서의 결과입니다. 어떤 수치도 미래 수익을 예측하지 않으며 특정 종목·전략을 권하지 않습니다. 과거 성과는 미래를 보장하지 않습니다.

9. 자주 묻는 질문

일봉 백테스트에서 손절과 익절이 같은 날 걸리면 어떻게 처리해야 하나요?

기본값은 손절이 먼저 체결된 것으로 보는 보수적 가정입니다. 다만 하나로 끝내지 말고 익절이 먼저인 낙관적 가정으로도 한 번 더 돌려 두 결과의 차이를 함께 봅니다. 차이가 크면 그 전략은 일봉으로 검증할 수 없으므로 분봉 데이터로 그날만 다시 판정해야 합니다.

backtesting.py와 backtrader는 같은 봉에서 손절·익절이 둘 다 닿으면 무엇을 체결하나요?

2026-10-12에 backtesting.py 0.6.6과 backtrader 1.9.78.123으로 시가 100, 고가 106, 저가 94인 봉에 손절 97·익절 105를 걸어 시험했을 때 두 라이브러리 모두 손절가 97에 청산했습니다. 버전에 따라 바뀔 수 있으니 쓰는 버전에서 같은 시험을 직접 돌려 보는 것이 안전합니다.

손절 폭을 넓히면 이 문제가 사라지나요?

줄어듭니다. 하루 변동폭이 손절 폭과 익절 폭의 합보다 커야 둘 다 닿기 때문입니다. 이 글의 대형주 10종목에서 손절 5%·익절 8%는 787건 중 0건, 코스닥 고변동 5종목은 362건 중 7건(1.9%)이었습니다. 반대로 1%·2%처럼 좁히면 코스닥 쪽은 41.0%까지 올라갔습니다.

갭으로 손절가 아래에서 시작하면 체결가는 얼마로 잡아야 하나요?

손절가가 아니라 그날 시가로 잡아야 합니다. 시가가 이미 손절가 아래라면 실제 스탑 주문은 시가 근처에서 체결됩니다. backtesting.py와 backtrader 모두 시험에서 시가 95(손절가 97)에 청산했습니다. 다만 실전에서는 시가 단일가 체결 뒤 호가 상황에 따라 더 불리할 수 있습니다.

손절·익절 처리까지 검증한 봇, 만들어 드립니다

전략 규칙만 알려 주시면 일봉·분봉 두 방식의 검증 결과를 같이 드립니다. 24시간 빠른 답변 가능합니다.

무료 상담 시작하기