무엇이 문제였나 — 밤을 넘긴 손절이 비싸 보였다
문제는 손실 자체보다, 손실을 줄이겠다는 규칙이 효과가 있는지 잴 수단이 없었다는 데 있었습니다. 우리 전략은 급등한 종목이 잠깐 밀릴 때 사서 며칠 들고 가는 단기 스윙입니다. 들고 가는 동안 가격이 미리 정한 손절선 아래로 내려가면 팔아서 손실을 끊습니다. 그런데 손절로 끝난 거래를 모아 보면, 진입 다음 날(D+1) 아침에 큰 손실로 끝나는 포지션이 눈에 띄었습니다.
자연스러운 처방은 「밤을 넘기기 전에 정리하자」입니다. 장 마감 직전인 15시 20분에, 손절선에 가까이 붙어 있는 포지션만 미리 팔면 다음 날 아침 갭 하락을 피할 수 있다는 생각입니다. 이 규칙이 맞다면 손실 한 덩어리를 통째로 지울 수 있고, 틀리다면 다음 날 반등할 포지션까지 싸게 팔아 버립니다. 어느 쪽인지는 숫자로 재야 했습니다.
직전 검토에서는 이 규칙의 효과를 +89.4%로 적어 두기도 했습니다. 숫자가 크고 선명해서, 남은 일은 백테스트로 확인하고 실매매에 넣는 순서만 남은 듯 보였습니다. 결과부터 말하면 이 +89.4%는 나중에 철회했고, 백테스트는 이 질문에 답할 수 있는 구조가 아니었습니다.
주식 자동매매에서 백테스트는 과거 가격 데이터 위에서 전략을 그대로 모의 실행해 성과를 재는 도구입니다. 규칙을 바꿀 때마다 실제 돈으로 몇 달씩 시험할 수 없으니, 거의 모든 채택·기각 판단이 백테스트 숫자에 기댑니다. 그래서 백테스트가 특정 상황을 구조적으로 못 보면, 그 상황을 겨냥한 규칙은 아무리 돌려도 효과가 0으로 나오거나 엉뚱한 값으로 나옵니다. 이번이 정확히 그 경우였습니다.
처음 잰 숫자 — 손절 57건 중 73.7%가 손절선을 뚫었다
백테스트로 15시 20분 청산 규칙을 재기 전에, 백테스트가 손절 청산을 어떻게 체결하는지부터 확인했습니다. 두 백테스트 실행기 모두 손절 청산 체결가로 손절선 가격을 그대로 쓰고 있었습니다. 코드로 보면 포지션 상태에 저장한 현재 손절가(`current_stop`)를 체결가 자리에 그대로 넣는 방식입니다.
이 방식에서는 밤사이 갭 하락이 존재하지 않습니다. 전날 종가가 손절선 위에 있고 다음 날 시가가 손절선보다 한참 아래에서 열려도, 백테스트는 손절선 가격에 정확히 팔았다고 기록합니다. 실제 시장에서는 시가가 이미 손절선 아래이므로, 아무리 빨리 팔아도 그보다 낮은 가격에 체결됩니다.
그래서 실제 손절 청산을 백테스트 체결가와 나란히 놓고 쟀습니다. 측정 구간은 2026년 5월 1일부터 8월 21일까지, 대상은 그 기간의 손절 청산 57건입니다. 그중 73.7%가 손절선을 갭으로 관통한 채 체결되어 있었고, 백테스트 체결가는 실제보다 평균 +4.96%p 낙관적이었습니다.

+4.96%p는 손절 한 건마다 실제보다 그만큼 덜 잃은 것으로 계산했다는 뜻입니다. 손절 규칙이나 밤사이 보유 규칙을 바꾸는 실험은 바로 이 갭에서 효과가 나와야 하는데, 백테스트는 그 갭을 지워 버린 상태였습니다. 갭이 없는 세계에서는 밤을 피하는 규칙이 이길 이유가 없습니다.
두 번째로 확인한 숫자는 발동 횟수입니다. 15시 20분 규칙에 손절선 근접도 임계 0.5, 1.0, 2.0을 걸어 돌리자 발동이 0회였습니다. 근접도와 상관없이 무조건 15시 20분에 파는 규칙도 1회만 발동했습니다. 단기 스윙에서 장 마감 직전 청산이 이렇게 드물 리가 없으니, 숫자가 아니라 엔진을 의심해야 했습니다.
어떤 가설 세 개를 세웠나?
검증 대상은 직전 검토에서 정해 둔 가설 세 개였습니다. 셋 다 「밤을 넘긴 손절 손실을 줄일 수 있다」는 같은 목표를 서로 다른 손잡이로 겨냥합니다.
- **가설 1 — 손절을 실행하는 방식을 바꾸면 덜 잃는다.** 손절 신호가 뜬 뒤 0분·3분·5분·10분 기다렸다 팔거나, 아예 종가까지 들고 가면 체결가가 나아진다는 생각입니다. 직전 검토는 이 효과의 상한을 +31%p로 적어 두었습니다.
- **가설 2 — 15시 20분에 손절선 근처 포지션만 정리하면 덜 잃는다.** 손절선과 현재가 사이 거리가 임계보다 가까운 포지션은 다음 날 갭으로 손절선을 뚫을 가능성이 크니, 장 마감 전에 미리 판다는 규칙입니다.
- **가설 3 — 10시대에 진입한 거래를 막으면 덜 잃는다.** 거래를 진입 시각별로 나누면 10시대 진입분 합계가 −37.6%로 나빠 보였고, 그 시간대 진입을 막는 게이트를 걸자는 생각입니다.
가설 2가 중심이었습니다. 직전 검토가 인용한 +89.4%도 이 규칙의 효과로 적혀 있었습니다. 원래 계획은 가설 2를 백테스트로 재고, 가설 1·3은 보조 측정으로 붙이는 순서였습니다.
세 가설 모두 그럴듯한 이유가 있었습니다. 갭 하락으로 손절선을 뚫는 손실이 실제로 많았으니, 그 손실이 생기기 전에 빠져나오는 규칙은 직관적으로 맞아 보입니다. 바로 그 직관 때문에 측정을 더 엄격하게 해야 했습니다. 맞아 보이는 규칙일수록 결과를 보고 규칙을 고르는 실수가 쉽게 섞입니다.
가설은 어디서 틀렸나?
첫 번째로 틀린 곳은 가설이 아니라 측정 방법이었습니다. 백테스트로 가설 2를 재려 했는데, 앞에서 본 갭 누락에 더해 엔진 구조가 15시 20분 규칙이 발동할 자리 자체를 만들지 않았습니다. 그래서 판정 기준을 백테스트에서 실제 거래의 반사실 재구성으로 바꾸고, 그 위에서 세 가설을 다시 쟀습니다.
**가설 1.** 대기 시간 0분·3분·5분·10분 네 경우 모두 t 통계량 절댓값이 0.6 미만이었습니다. 종가까지 들고 간 경우도 +0.06%p, t=+0.10으로 차이가 없었습니다. 유일하게 개선으로 보인 열은 슬리피지(주문가와 실제 체결가의 차이)를 0으로 둔 계산이었는데, 이건 주문 유형으로 얻을 수 있는 몫이 아닙니다. 실매매는 이미 최유리지정가(호가창의 가장 유리한 반대편 가격으로 내는 지정가 주문)를 쓰고 있었기 때문입니다. 그래서 「상한 +31%p」 추정을 철회했습니다.
**가설 2.** 근접도 임계 5개를 걸어 봤더니 4개가 아무 규칙도 없는 기준선보다 나빴습니다. 더 나쁜 신호는 부호가 임계에 따라 뒤집힌다는 점이었습니다. 임계 순서대로 −49, −43, −44, +7, −8(단위 %p)로, 한 임계에서만 플러스가 나오고 앞뒤는 마이너스입니다. 실제로 효과가 있는 규칙이라면 임계를 조금 옮겨도 방향은 유지되어야 합니다.

근접도가 실제로 무엇을 가려내는지도 따로 쟀습니다. 손절선에 가까운 포지션이 다음 날(D+1) 손절로 끝나는지, 그다음 날(D+2) 이후에 끝나는지를 맞히는 정밀도가 54~61%였습니다. 아무렇게나 골랐을 때의 기준선이 44.8%이니 조금 높아 보이지만, 단측 이항 검정 p값이 0.071~0.126으로 우연과 구별하지 못했습니다. 근접도는 D+1과 D+2를 가르지 못했습니다.
**가설 3.** 10시대 진입분의 −37.6%를 다시 뜯어보자, 그 안에서 D+1 손절을 빼면 +7.1%로 부호가 바뀌었습니다. 10시대가 나쁜 이유는 시간대가 아니라 D+1 손절이 몰려 있어서였고, 그 문제는 가설 2의 질문과 같습니다. 그런데도 10시대 진입 게이트를 따로 걸면 그 시간대의 흑자 거래 12건, 합계 +61.9%까지 버려서 총액이 −0.8%p 나빠졌습니다. 가설 3은 가설 2에 흡수해 기각했습니다.

진짜 원인은 무엇이었나?
원인은 두 층이었습니다. 겉층은 앞에서 본 체결가 문제이고, 속층은 시뮬레이션 루프의 순서였습니다. 그리고 그 위에, 처음부터 효과를 부풀려 보이게 만든 계산 실수가 하나 얹혀 있었습니다.
**겉층 — 갭이 없는 체결 모델.** 손절 청산을 손절선 가격에 체결하는 모델은 밤사이 갭을 지웁니다. 57건 중 73.7%가 갭으로 손절선을 뚫은 기간에, 평균 +4.96%p만큼 손절 손실을 작게 보여 줍니다. 밤을 피하는 규칙이 얻을 이익이 정확히 이 갭이므로, 이 모델 위에서는 그 이익이 사라집니다.
**속층 — 진입일에 청산을 평가하지 않는 루프.** 분봉 시뮬레이션 함수 `run_minute_simulation()`은 하루를 ①기존 포지션 청산 감시 → ②신규 진입 순서로 처리합니다. 청산 감시 코드가 신규 진입 코드보다 앞에 놓여 있어서(실행기 파일의 412번째 줄과 621번째 줄), 그날 새로 들어간 포지션은 그날 청산 감시를 한 번도 받지 않습니다. 진입 당일(D+0) 청산 구간이 구조적으로 0건이고, 15시 20분 규칙은 진입 당일 포지션에 발동할 자리가 없습니다. 임계 0.5·1.0·2.0에서 발동 0회, 무조건 청산 1회라는 숫자가 이 구조의 결과였습니다.

**계산 실수 — +89.4%의 정체.** 직전 검토가 인용한 +89.4%는 D+1 손절 거래를 표본에서 빼고 나머지를 다시 더한 값이었습니다. 15시 20분에 실제로 판 결과가 아닙니다. 실제로 15시 20분에 팔면 그 시점 가격이 손익으로 확정되고, 그 가격은 다음 날 갭 하락만큼 나쁘지는 않아도 공짜도 아닙니다. 오버나잇 포지션 67건 전체에 15시 20분 매도를 실제로 적용하면 −0.12%p, t=−0.16으로 효과가 0입니다.
"+89.4%"는 달성 가능한 목표가 아니라 산술의 성질이었다
나쁜 결과를 표본에서 빼면 남은 숫자는 당연히 좋아집니다. 문제는 어떤 포지션이 D+1 손절로 끝날지 15시 20분에는 알 수 없다는 점입니다. 사후에만 아는 정보로 표본을 자르면, 규칙의 효과가 아니라 자르는 행위의 효과를 재게 됩니다.
조치 — 판정 기준을 실제 거래 재구성으로 바꿨다
이번 검증의 판정 기준을 실제 거래의 반사실 재구성으로 정했습니다. 이미 일어난 청산마다 그날 분봉을 다시 열어 「15시 20분에 팔았다면」, 「5분 기다렸다 팔았다면」 같은 대안의 체결가를 실제 가격으로 계산합니다. 채택·기각은 이 결과로 정하고, 백테스트는 보조로만 씁니다.
백테스트를 같이 고치지 않은 데도 이유가 있습니다. 검토한 대안 네 가지와 결론은 다음과 같습니다.
| 대안 | 결론 | 사유 |
|---|---|---|
| 백테스트 엔진의 진입일 평가를 지금 고치고 다시 돌린다 | 연기 | 진입·청산 타이밍이 바뀌어 과거 백테스트 결과 전체가 흔들린다. 같은 날 체결 모델도 바꿨으니 두 변경이 겹치면 어느 쪽 효과인지 가를 수 없다 |
| 유일하게 플러스인 3.0%p 임계만 채택한다(+7.3%p) | 기각 | 부호가 임계마다 뒤집히는 구간에서 하나만 고르면 결과를 보고 규칙을 고르는 셈이다. 이 표본은 3건이 27.5%p를 움직인다 |
| D+1이 될 포지션만 골라 자른다 | 실행 불가 | 어느 포지션이 D+1이 될지는 사후에만 안다. 근접도가 그 예측력을 주지 못했다 |
| 판정 불가로 두고 다음에 백테스트로 다시 잰다 | 기각 | 실제 거래 재구성이 이미 t=−0.16으로 답을 냈다. 백테스트를 고쳐도 같은 데이터를 다른 길로 볼 뿐이다 |
3건이 27.5%p를 움직이는 표본에서 +7.3%p는 잡음 폭 안에 있다.
백테스트 쪽 조치는 체결 모델 하나로 한정했습니다. 손절 청산 체결가로 손절선 가격을 그대로 쓰던 방식을 버리고, 갭을 지우지 않는 쪽으로 체결 모델을 정직하게 고쳤습니다. 진입일 청산 평가는 지금 결과를 대조군으로 저장해 두고 별도 작업으로 넘겼습니다. 두 변경을 한꺼번에 넣으면 무엇이 무엇을 바꿨는지 다시 모르게 됩니다.
실매매 청산 코드는 바꾸지 않았습니다. 변경 0건입니다. 대신 측정 쪽 두 곳을 고쳤습니다. 거래 기록(저널)이 분석 파이프라인으로 넘어가는 배선과, 테스트가 실제 데이터에 섞이지 않도록 분리하는 격리입니다. 규칙을 넣지 않은 결정도 결정이고, 다음 실험이 같은 함정에 빠지지 않게 하는 데 이 두 수리가 더 쓸모 있었습니다.
재측정 결과 — 무엇이 달라졌나?
숫자로 보면 「손실이 줄었다」는 결과는 없습니다. 달라진 것은 무엇을 믿을지였습니다. 측정 전과 후를 나란히 놓으면 다음과 같습니다.
인용값은 사후에만 아는 D+1 손절을 표본에서 제외한 계산이다. 실제로 그 시각 가격에 팔면 효과는 0이다.
가설 1은 대기 0·3·5·10분 모두 |t| 0.6 미만, 종가 보유 +0.06%p로 효과가 없었고, 이전에 적은 상한 +31%p를 철회했습니다. 가설 2는 임계 5개 중 4개가 기준선보다 나빴고, 근접도 정밀도 54~61%는 무작위 기준선 44.8%와 통계적으로 구별되지 않았습니다(p=0.071~0.126). 가설 3은 D+1을 빼면 −37.6%가 +7.1%로 바뀌는 문제였고, 게이트를 걸면 총액이 −0.8%p 나빠졌습니다.
백테스트 체결 모델도 손절선 가격을 그대로 쓰지 않도록 고쳤습니다. 손절 청산 57건에서 평균 +4.96%p만큼 손실을 작게 보이던 낙관의 출처를 체결 모델에서 직접 막았습니다. 앞으로 손절·보유 규칙을 백테스트로 잴 때, 밤을 피하는 규칙이 얻을 몫이 계산에서 사라지지 않습니다.
가장 큰 변화는 직전 검토의 +89.4%가 공식 기록에서 지워졌다는 점입니다. 그 숫자가 남아 있었다면 다음 검토는 「효과 +89.4%짜리 규칙을 왜 안 넣었나」에서 출발했을 겁니다. 측정을 다시 하지 않았다면 이 숫자는 근거처럼 계속 인용됐을 가능성이 큽니다. 비슷하게 작은 표본에서 결론이 뒤집힌 다른 사례는 표본이 작을 때 결론이 뒤집힌 기록에 따로 적어 두었습니다.
남는 한계
실제 거래 재구성에도 약점이 있습니다. 표본이 작습니다. 오버나잇 포지션이 67건뿐이라 웬만한 효과는 「유의하지 않음」으로 나옵니다. 체결 모델도 근사입니다. 분봉 가격으로 대안 체결가를 계산하므로, 그 시각 호가창에서 실제로 그 가격에 다 팔렸을지는 확인하지 못합니다.
그래도 이 방법을 정본으로 둔 이유는 실제로 일어난 가격을 쓰기 때문입니다. 백테스트처럼 구조적으로 한쪽으로 기우는 낙관이 없습니다. 이 크기에서 「유의하지 않음」이 많이 나오는 결과는 방법의 결함이라기보다, 데이터가 아직 말할 수 있는 만큼만 말하고 있다는 뜻으로 읽었습니다.
백테스트의 진입일 청산 평가는 아직 비어 있습니다. 진입 당일 청산 구간이 0건인 구조는 그대로라, 진입 당일에 작동하는 규칙은 지금 백테스트로 잴 수 없습니다. 이 수리는 과거 결과를 한 번 더 흔들기 때문에, 대조군 저장 뒤 별도 사이클에서 진행합니다.
다음 실험의 축도 바꿨습니다. 시각(15시 20분, 10시대)이 아니라 변동성과 갭 위험을 기준으로 거는 게이트, 전량 청산이 아니라 밤을 넘길 포지션 크기를 줄이는 방법, 그리고 보유 중 최대 이익·최대 손실(MFE/MAE) 기록이 충분히 쌓인 뒤의 네 번째 가설입니다. 셋 다 이번 기각 결과가 남긴 질문에서 나왔습니다.
백테스트 결과를 믿기 전에 무엇을 확인해야 하나?
이번 일에서 다른 자동매매 코드에도 그대로 쓸 수 있는 점검 항목을 네 가지로 정리했습니다. 전략 종류와 상관없이, 손절·보유·청산 시각을 다루는 규칙을 재기 전에 먼저 확인할 만합니다.
- **손절 체결가가 손절선 가격 그대로인가.** 그렇다면 갭 하락이 없는 세계를 계산하고 있습니다. 실제 손절 청산과 백테스트 체결가를 건별로 대조해 몇 %가 갭으로 손절선을 뚫었는지부터 셉니다. 우리는 57건 중 73.7%였습니다.
- **규칙이 실제로 몇 번 발동하는가.** 효과를 보기 전에 발동 횟수를 봅니다. 0회나 1회라면 규칙이 아니라 엔진 구조를 의심합니다.
- **시뮬레이션 루프 순서가 진입 당일 청산을 허용하는가.** 청산 감시가 신규 진입보다 앞에 있으면 진입 당일 포지션은 청산 평가를 받지 않습니다.
- **좋아 보이는 숫자가 표본을 자른 값인가, 규칙을 실행한 값인가.** 사후에만 아는 조건으로 표본을 빼면 숫자는 언제나 좋아집니다. 그 시각 가격으로 실제로 팔았을 때의 값을 따로 계산합니다.
마지막으로, 결과가 임계에 따라 부호를 바꾸면 그중 하나를 고르지 않습니다. 이번 가설 2에서 3.0%p 임계 하나만 +7.3%p였지만, 3건이 27.5%p를 움직이는 표본에서 그 숫자는 잡음 폭 안에 있었습니다. 규칙을 넣지 않고 끝난 검증도, 다음 판단을 덜 틀리게 만든다면 충분히 값을 합니다.
자동화 시스템에서 측정 도구부터 의심해야 하는 상황이 비슷하게 막혀 있다면, 어떤 숫자를 먼저 재야 하는지 함께 정리해 드릴 수 있습니다. 상담에서 지금 쓰는 측정 방식과 막힌 지점을 적어 주시면 됩니다.