내 화면이 내 규칙과 다른 말을 하고 있었다
내 화면이 내 규칙과 다른 말을 하고 있었다
규칙 기반 매매 시스템을 만들었다면, 확인해야 할 게 하나 있다. 화면에 뜨는 숫자가 정말 그 규칙에서 나온 숫자인가.
내 시스템에서 세 개가 아니었다. 그리고 그건 에러를 내지 않았다.
첫 번째: 존재하지 않는 익절가
보유 종목 대시보드에 "익절목표" 칸이 있었다. 계산식은 이랬다.
```
목표가 = 현재가 + 2 × (현재가 − 20일선)
그리고 손실 중이면 → 매입가 × 1.1 로 대체
```
문제가 네 겹이었다.
하나, 검증된 규칙에는 익절가라는 개념 자체가 없다. 청산은 추세 트레일링이고 고정 목표를 두지 않는다. 화면에만 있는 숫자였다.
둘, 손절선과 기준이 달랐다. 규칙의 손절은 `min(직전 5봉 저점, 20일선×0.98)`인데, 이 목표가는 `현재가 − 20일선`을 1R로 썼다. 같은 "2R"이라는 말을 쓰면서 R의 정의가 서로 달랐다.
셋, 진입가가 아니라 현재가 기준이라 매일 움직였다. 주가가 오르면 목표도 따라 올라가서 영원히 닿지 않는다.
넷, 이게 제일 나쁘다. `손실 중이면 매입가 × 1.1`. 물려 있으면 목표를 "최소 10% 회복"으로 바꿔치기하는 로직이다. 이건 규칙이 아니라 손실 회피 심리를 코드로 굳힌 것이다. 감정을 배제하려고 만든 시스템에 감정이 들어가 있었다.
같은 코드가 세 군데 더 있었다. 종목 분석 화면에는 `종가 + 2R`이, 텔레그램 브리핑에는 `여력 +N%`가 있었다. 전부 백테스트로 검증된 적 없는 숫자였다.
화면을 보고 파는 사람과 규칙대로 파는 사람이 서로 다른 매매를 하게 된다. 혼자 쓰는 시스템에서도 이건 치명적이다. 오늘의 나와 3주 전의 내가 다른 규칙으로 매매하게 되니까.
두 번째: 조용히 무시되던 파라미터
청산 채널을 10일에서 15일로 바꿔서 백테스트를 돌렸다. 결과가 나왔다.
```
채널 10 → 8,347건, 기대값 0.093, MDD −131.6R, 보유 11일
채널 15 → 8,347건, 기대값 0.093, MDD −131.6R, 보유 11일
```
완전히 똑같았다. 건수도, 소수점 셋째 자리까지도.
우연일 리가 없어서 한 종목만 떼어 청산 사유를 찍어봤다.
```
channel=10 → trail 14건, supportBreak 0건
channel=15 → trail 4건, supportBreak 10건
channel=30 → trail 0건, supportBreak 14건
```
원인이 나왔다. 청산 경로가 두 개였다.
```ts
// 경로 1 — 파라미터를 따름
trailLevelAt → swingLow(low, i, profile.trail.channel)
// 경로 2 — sellFlags 안에, 10이 하드코딩
if (want.has("supportBreak") && c < swingLow(low, i, 10)) ...
```
둘 다 "10일 저점 이탈"이라 완전히 중복이었다. 그래서 채널을 10보다 넓히면 하드코딩된 쪽이 먼저 걸려서 파라미터가 무시된다. 숫자를 바꿔도 아무 일도 일어나지 않았던 것이다.
이게 왜 중요하냐면, 이 버그 때문에 "청산은 10일이 최적"이라는 결론 자체가 검증된 적이 없었다. 넓혀서 비교해보려 해도 애초에 비교가 안 됐으니까. 중복을 제거하고 다시 재보니 채널 45가 10보다 50% 나았다.
세 번째: 브리핑이 같은 목록을 두 번 보내고 있었다
아침에는 한국 시장 브리핑, 저녁에는 미국 시장 브리핑을 텔레그램으로 보낸다. `?market=kr`, `?market=us` 파라미터로 구분한다.
그런데 종목 목록을 뽑는 코드에 시장 필터가 없었다.
```ts
const setups = cache.setups.filter(s => s.tier === "BUY"); // 시장 구분 없음
```
`market` 파라미터는 지수 표시(코스피/나스닥)만 바꾸고 있었다. 종목 목록은 아침이나 저녁이나 똑같은 게 나갔다.
공통점
세 가지 모두 에러를 내지 않았다.
```
가짜 익절가 → 그럴듯한 숫자가 화면에 떴다
파라미터 무시 → 백테스트가 정상적으로 돌아 결과를 냈다
시장 필터 누락 → 브리핑이 매일 잘 발송됐다
```
프로그램이 멈추거나 빨간 글씨를 뱉었다면 바로 고쳤을 것이다. 아무 일 없이 잘 도는 게 문제였다.
찾아낸 계기도 전부 비슷했다. 숫자가 이상해서 파고들었다. 채널 10과 15가 완전히 같길래, 브리핑 내용이 아침저녁 똑같길래, 익절가가 매일 바뀌길래.
그래서 얻은 습관
결과가 나오면 개수부터 센다. 같은 값이 나오면 안 되는 자리에 같은 값이 나오는지, 건수가 예상 범위인지.
실제로 이 원칙이 그날 한 번 더 통했다. 20년치 데이터를 받는데 503종이 전부 "데이터 부족"으로 처리됐다. 확인해보니 야후에 `range=max`를 넣으면 일봉이 아니라 월봉을 돌려준다. 168봉짜리 월봉을 일봉인 줄 알고 백테스트를 돌릴 뻔했다. 이것도 에러는 없었다.
규칙 기반 매매의 전제는 "규칙대로 돌아간다"는 믿음이다. 그 믿음은 확인해야 유지된다.
핵심 요약
1. 화면의 익절가가 검증된 규칙에서 나온 값이 아니었다. 세 군데에 있었다.
2. 손실 중이면 목표를 "매입가 ×1.1"로 바꾸는 로직이 있었다. 감정을 배제하려는 시스템에 회피 심리가 코드로 들어가 있었다.
3. 청산 로직이 두 곳에 중복돼 파라미터가 무시되고 있었다. 채널 값을 바꿔도 결과가 같았다.
4. 브리핑에 시장 필터가 없어 같은 목록이 두 번 발송됐다.
5. 셋 다 에러를 내지 않았다. 잘 도는 게 문제였다.
자주 묻는 질문
*이 글의 코드와 수치는 실제 프로젝트에서 발견하고 수정한 내용이다.*