상한가에 매수됐다고 계산해도 될까? 가격제한폭·거래정지 체결 검증

백테스트에는 매수 가격이 분명히 찍혀 있는데 실제 주문은 한 주도 체결되지 않을 수 있습니다. 거래정지일에도 데이터 제공처가 전일 종가로 시가·고가·저가·종가를 채워 놓을 수 있고, 상한가에서 거래가 발생했더라도 내 주문 앞에 쌓인 대기 물량까지 일봉으로 확인할 수 없기 때문입니다.

이 오류는 고정 슬리피지를 조금 더 넣는 것으로 해결되지 않습니다. 슬리피지는 체결 가격이 얼마나 불리해지는지를 다루지만, 거래정지와 상한가 주문 대기열은 체결 자체가 있었는가를 먼저 묻습니다.

2026년 국내 주식 일봉과 종목 상태 자료에서 급등 신고가 신호 835건을 검사했습니다. 다음 거래일이 거래정지이면서 거래량 0이었던 신호는 13건, 하루 종일 상한가 한 가격에서만 거래된 신호는 5건이었습니다. OHLC만 사용하면 이 18건도 모두 체결된 거래가 됩니다.

먼저 확인할 결론

  • 급등 신고가 신호 835건 가운데 817건만 다음 거래일에 일반적인 가격 범위에서 거래됐습니다.
  • 거래정지·거래량 0인 13건에는 실제로 살 수 없는 가격이 일봉 시가처럼 남아 있었습니다.
  • 상한가 한 가격에서 거래된 5건은 시장 거래량이 있어도 내 주문의 체결 여부를 일봉만으로 확정할 수 없습니다.
  • 가짜 체결을 모두 허용한 평균수익률은 -1.96%, 주문 상태를 반영한 비교는 -2.15%~-2.20%였습니다.

종가 신호를 바로 샀더니 백테스트가 틀어진 이유가 신호 확정 시점과 주문 시점을 분리했다면, 이번 글은 주문 시점이 올바르더라도 그 가격에 실제로 체결될 수 있었는지를 확인합니다.

가격이 기록됐다는 사실과 체결은 다릅니다

KRX 주식시장 거래 안내는 일반 주식의 일일 가격제한폭을 기준가격 대비 ±30%로 설명합니다. KRX 기준가격 설명에 따르면 기준가격은 일반적으로 전일 종가이며, 기업행사나 장기 거래정지 뒤 재개처럼 예외적인 경우에는 별도의 조정 절차가 적용됩니다.

가격제한폭 안에 주문을 냈다고 곧바로 체결되는 것은 아닙니다. KRX 연속매매 체결 원칙은 가격 우선과 시간 우선에 따라 주문을 매칭하며, 제시된 예시에도 일부 수량이 미체결로 남습니다. 시가를 정하는 단일가매매도 주문 수량을 모아 가장 많은 수량이 체결되는 가격을 정하므로, 차트에 시가가 생겼다는 사실만으로 내 주문 전량이 그 가격에 체결됐다고 볼 수 없습니다.

이번 검증에서는 다음 세 상태를 분리했습니다.

다음 거래일 상태 일봉에서 보이는 모습 백테스트 처리
일반 거래 거래량이 있고 가격 범위가 열림 체결 후보로 인정
거래정지·거래량 0 OHLC는 남아 있지만 거래량이 0 체결 불가
상한가 한 가격 시가·고가·저가·종가가 모두 상한가이고 거래량은 존재 일봉만으로 내 주문 체결을 확정할 수 없어 보수적으로 제외 또는 지연

상한가 한 가격을 무조건 미체결로 단정한 것은 아닙니다. 그날 실제 거래량이 있으므로 먼저 들어간 주문 일부는 체결됐습니다. 다만 주문 시각, 주문 수량, 당시 매도 잔량과 대기 순서를 모르는 백테스트가 내 주문까지 전량 체결됐다고 단정할 근거도 없습니다. 따라서 결과표에서는 체결 불확실로 분류했습니다.

급등 신고가 신호를 같은 조건으로 비교했습니다

단순한 가격 상승보다 체결 제약이 자주 나타나는 표본을 보기 위해 급등·신고가 조건을 사용했습니다. 이 규칙의 투자 성과를 추천하려는 실험이 아니라, 동일한 주문 신호를 체결 모형만 바꿔 비교하는 진단용 이벤트 테스트입니다.

항목 검증 조건
상태 자료 관측 기간 2026년 1월 14일~9월 7일, 159거래일
신호 관측 기간 2026년 1월 30일~8월 28일
대상 국내 주권 중 정상·일반 종목
제외 우선주, SPAC, 관리종목, 정리매매 종목
유동성 직전 20거래일 거래대금 중앙값 10억 원 이상
신호 일간수익률 20% 이상이면서 직전 20거래일 종가 최고치 돌파
주문 신호 확정 다음 거래일 시가 매수
보유 진입 뒤 5거래일, 이후 시가 청산
비용 매수·매도 각각 0.10%, 총 0.20%
가격 기준 신호·수익률은 수정 가격, 상·하한가 판정은 당시 원가격

같은 종목에서 조건이 다시 발생하면 별개의 신호로 기록했으므로 835건은 서로 완전히 독립적인 거래가 아닙니다. 여러 종목의 포지션이 동시에 겹칠 수도 있습니다. 따라서 누적수익률, CAGR, MDD를 억지로 만들지 않고 신호별 5거래일 수익률의 평균·중앙값·승률을 비교했습니다.

체결 모형은 세 가지입니다.

  1. OHLC만 보고 전부 체결: 거래량과 종목 상태를 무시하고 다음 행의 시가에 매수·매도했다고 계산합니다.
  2. 불가·불확실 주문 취소: 거래정지·거래량 0과 상한가 한 가격의 매수 주문을 취소합니다.
  3. 첫 일반 거래일까지 지연: 주문을 취소하지 않고 최대 10거래일 동안 거래 가능한 첫 시가까지 기다립니다.

매도도 같은 원칙으로 거래정지·거래량 0과 하한가 한 가격을 확인했습니다. 정상 진입 817건 가운데 한 건은 예정 청산일부터 10거래일 안에 청산 조건을 충족하지 못해 실현수익률 비교에서 제외했습니다. 지연 시나리오에서도 같은 한 건이 제외돼 완료 거래는 834건입니다.

835건 중 18건은 가격만 보고 체결을 인정할 수 없었습니다

다음 거래일 진입 상태 신호 수 비중 기록된 시가 체결 가정의 평균수익률 중앙값
일반 거래 817건 97.84% -2.28% -7.07%
거래정지·거래량 0 13건 1.56% +22.93% -3.80%
상한가 한 가격 5건 0.60% -14.60% -13.50%
합계 835건 100.00% -1.96% -7.07%

비중만 보면 18건, 2.16%가 작아 보입니다. 하지만 거래정지 13건을 체결됐다고 가정한 평균수익률은 +22.93%였습니다. 중앙값은 -3.80%인데 일부 존재하지 않는 큰 이익이 평균을 위로 끌어올렸습니다. 체결 오류는 모든 거래를 조금씩 바꾸기보다 소수의 꼬리 수익을 크게 왜곡할 수 있습니다.

상한가 한 가격의 5건은 모두 마감 스냅샷에서 최우선 매도호가가 0이었습니다. 이것만으로 시가 주문의 미체결을 증명할 수는 없지만, 하루 거래량과 OHLC만으로 주문 대기열을 복원할 수 없다는 점은 분명합니다.

급등 신고가 신호의 다음 거래일 상태와 체결 모형별 5거래일 수익률 비교

평균 차이는 작았지만 그 이유는 중요했습니다

체결 모형 완료 거래 평균수익률 중앙값 승률 최악 거래
OHLC만 보고 전부 체결 835건 -1.96% -7.07% 36.77% -51.51%
불가·불확실 주문 취소 816건 -2.15% -7.04% 36.89% -51.51%
첫 일반 거래일까지 지연 834건 -2.20% -7.10% 36.93% -51.51%

OHLC만 사용한 결과와 주문 취소 결과의 평균 차이는 0.19%포인트, 지연 체결과의 차이는 0.24%포인트였습니다. 중앙값과 승률은 거의 같았습니다. 이 숫자만 보면 체결 조건을 생략해도 된다고 생각하기 쉽습니다.

그러나 평균이 좋아진 이유는 전략이 더 정교해서가 아닙니다. 거래가 한 주도 없던 정지일의 채워진 시가를 매수 가격으로 사용한 뒤, 재개 이후 상승을 가져왔기 때문입니다. 표본과 전략이 달라지면 오류의 방향도 달라집니다. 하한가에 매도했다고 가정한 전략에서는 손실이 과소평가될 수 있습니다.

개별 사례에서는 차이가 훨씬 컸습니다.

신호 종목 다음 거래일 상태 기록된 시가 체결 가정 첫 일반 거래일까지 지연 차이가 생긴 이유
이노인스트루먼트 거래정지·거래량 0 +182.21% +94.60% 정지일 가격을 진입가로 사용해 재개 전 상승을 가져옴
삼표시멘트 상한가 한 가격 -16.36% -39.82% 상한가 주문을 미룬 뒤 진입 가격과 관찰 구간이 모두 변경
에스에이엠티 상한가 한 가격 -13.11% +11.17% 하루 지연만으로 손익 방향까지 반전
가온전선 거래정지·거래량 0 +38.22% +8.52% 정지일 체결 가정이 보유 구간을 앞당김

지연 체결 결과가 언제나 더 나쁜 것도 아닙니다. 에스에이엠티처럼 손실이 이익으로 바뀐 사례도 있습니다. 이것은 지연 규칙이 전략을 개선했다는 뜻이 아니라, 진입일이 달라지는 순간 같은 거래가 아니게 된다는 뜻입니다. 주문이 막혔을 때 취소할지, 다음 날 다시 낼지, 신호를 다시 계산할지 전략 규칙에 미리 적어야 합니다.

체결 필터는 거래량 0을 지우기 전에 적용합니다

가장 위험한 코드는 volume > 0인 행만 남긴 뒤 종목별 다음 행을 진입일로 쓰는 방식입니다. 거래정지 행이 먼저 사라지면 원래 다음 거래일 주문이 며칠 뒤 재개일 주문으로 조용히 바뀝니다. 지연이라는 전략 규칙을 쓰지 않았는데 데이터 정리 과정이 지연 체결을 만들어 버립니다.

처리 순서는 다음과 같아야 합니다.

  1. 거래소 거래일 기준으로 종목별 행을 유지합니다.
  2. 당일 종가가 끝난 뒤 신호를 확정합니다.
  3. 다음 거래일의 종목 상태와 거래량을 결합합니다.
  4. 매수는 거래정지·거래량 0·상한가 고정, 매도는 거래정지·거래량 0·하한가 고정을 검사합니다.
  5. 미체결 시 취소·지연·재주문 가운데 하나를 명시적으로 선택합니다.
  6. 주문 수량이 당일 거래량이나 호가 잔량에 비해 크지 않은지도 별도로 확인합니다.

다음 코드는 이미 수집한 일봉·종목 상태 표에 다음 거래일 매수 필터를 붙이는 핵심 부분입니다. open, high, low, close, upper_limit는 모두 당시 원가격 기준이어야 합니다. 제작용 차트와 파일 저장 코드는 제외했습니다.

PYTHON
"""일봉 백테스트에서 다음 거래일 주문의 체결 조건을 분리하는 예시."""

import numpy as np
import pandas as pd


LOOKBACK_SESSIONS = 20
MIN_SIGNAL_RETURN = 0.20
MIN_MEDIAN_TRADED_VALUE = 1_000_000_000
BUY_SLIPPAGE = 0.0005


def add_execution_conditions(market_data: pd.DataFrame) -> pd.DataFrame:
    required_columns = {
        "code",
        "date",
        "open",
        "high",
        "low",
        "close",
        "volume",
        "traded_value",
        "stock_state",
        "upper_limit",
    }
    missing_columns = required_columns.difference(market_data.columns)
    if missing_columns:
        raise ValueError(f"필요한 열이 없습니다: {sorted(missing_columns)}")

    result = market_data.sort_values(["code", "date"]).copy()
    grouped = result.groupby("code", sort=False)
    result["daily_return"] = grouped["close"].pct_change(fill_method=None)
    result["prior_high"] = grouped["close"].transform(
        lambda price: price.shift(1).rolling(LOOKBACK_SESSIONS).max()
    )
    result["prior_median_value"] = grouped["traded_value"].transform(
        lambda value: value.shift(1).rolling(LOOKBACK_SESSIONS).median()
    )
    result["signal"] = (
        result["daily_return"].ge(MIN_SIGNAL_RETURN)
        & result["close"].ge(result["prior_high"])
        & result["prior_median_value"].ge(MIN_MEDIAN_TRADED_VALUE)
    )

    result["locked_upper"] = (
        result["open"].eq(result["upper_limit"])
        & result["high"].eq(result["upper_limit"])
        & result["low"].eq(result["upper_limit"])
        & result["close"].eq(result["upper_limit"])
        & result["volume"].gt(0)
    )
    result["buy_order"] = grouped["signal"].shift(1).fillna(False)
    result["buy_executable"] = (
        result["stock_state"].eq("정상")
        & result["volume"].gt(0)
        & result["open"].gt(0)
        & ~result["locked_upper"]
    )
    result["buy_filled"] = result["buy_order"] & result["buy_executable"]
    result["entry_price"] = np.where(
        result["buy_filled"],
        result["open"] * (1.0 + BUY_SLIPPAGE),
        np.nan,
    )
    return result


# 거래량이 0인 행을 먼저 삭제하면 거래정지일을 건너뛴 뒤
# 재개일에 주문이 체결된 것처럼 바뀔 수 있으므로 상태 판정 뒤에 처리합니다.

코드에서 중요한 것은 거래량 0인 행을 먼저 삭제하지 않는 점입니다. buy_order를 만든 뒤 같은 행의 stock_state, volume, locked_upper를 확인해야 원래 주문일을 보존할 수 있습니다. 실제 백테스트에서는 매도 주문에도 locked_lower 검사를 대칭으로 적용하고, 미체결 주문의 유효기간과 부분 체결 정책을 추가해야 합니다.

일봉으로 확인할 수 있는 범위에는 한계가 있습니다

데이터 수준 확인할 수 있는 것 확인할 수 없는 것
일봉 OHLCV 당일 가격 범위와 전체 거래량 내 주문 시점의 잔량과 대기 순서
종목 상태·상하한가 거래정지 여부와 가격제한폭 고정 내 주문의 실제 체결 수량
분·틱·호가 데이터 시간대별 거래와 매도·매수 잔량 변화 내 주문이 어느 순서에 있었는지
증권사 주문 로그 접수·부분 체결·미체결·취소 다른 투자자의 전체 대기 주문

따라서 일봉 백테스트가 할 수 있는 최선은 체결 확정이 아니라 보수적인 체결 가능 후보를 만드는 일입니다. 상한가 한 가격에서 무조건 전량 체결을 허용하면 낙관적이고, 전부 취소하면 보수적입니다. 두 결과를 함께 표시하거나 실제 주문 로그로 체결률을 추정해야 범위를 알 수 있습니다.

수수료·세금·슬리피지까지 반영한 백테스트에서 다룬 비용은 이 체결 검사를 통과한 주문에 적용해야 합니다. 거래가 불가능한 주문에 수수료와 슬리피지만 차감한다고 실전 모델이 되지는 않습니다.

수익률보다 체결 로그를 먼저 봅니다

이번 검증에서 확인한 내용은 세 가지입니다.

  1. 값이 채워진 OHLC는 거래 가능성을 보장하지 않습니다.
  2. 거래정지는 체결 불가로, 상한가 한 가격은 체결 불확실로 구분해야 합니다.
  3. 전체 평균 차이가 작아도 소수의 가짜 큰 수익이 전략 평가를 바꿀 수 있습니다.

수정주가를 잘못 쓰면 백테스트가 어떻게 망가지는지는 신호·체결·성과 평가 가격을 나누는 이유를 설명합니다. 백테스트 결과를 믿기 전 확인할 세 가지와 함께 보면 수익률 표보다 먼저 확인해야 할 데이터 조건을 정리할 수 있습니다.

이 글은 과거 일봉과 단순 신호를 이용한 교육용 체결 검증입니다. 실제 체결은 주문 시각, 주문 유형, 호가 잔량과 주문 수량에 따라 달라지며 특정 종목의 매수·매도를 권하지 않습니다.