Thu 08-20 · 오늘 5건 · 통산 1,496호
falette
스크랩 쪽지 로그인
업비트 자동매매 봇 만들기
 

**⑩ 트레일링 스탑 — 부검(Autopsy)**

9편에서 범인을 확정했다. 트레일링 스탑. 하지만 확정과 이해는 다른 문제다. 정확히 어느 줄에서, 어떤 조건이 겹쳐서 손절 신호가 사라졌는지 — 그건 실제로 코드에 메스를 대야만 알 수 있는 일이었다.

현장 재방문

범인을 지목했다고 사건이 끝난 게 아니다. 어느 줄에서, 어떤 조건이 겹쳐서 신호가 사라졌는지 — 그 구체적인 지점을 짚지 못하면 다음 편에서 뭘 고쳐야 할지도 알 수 없다. 그래서 코드를 다시 열었다. check_risk(). 실거래 매매마다 이 함수 하나가 호출되고, 이 함수 하나가 "괜찮다" 또는 "손절해라"를 판단한다.

위쪽부터 순서대로 읽었다. 손실률이 레짐별 기본 손절선을 넘으면 무조건 손절 신호가 나간다. 이건 살아있었다. 즉 9편에서 걱정했던 "애초에 손절이 하나도 없다"는 최악의 시나리오는 아니었다는 뜻이다. 안도할 뻔했다. 그렇다면 진짜 문제는 이 아래, 좀 더 깊은 곳에 있어야 했다.

두 번째 방어선

기본 손절 아래에는 두 번째 방어선이 하나 더 있었다. 트레일링 스탑 — 매수가가 아니라 진입 이후 찍은 고점을 기준으로, 거기서 일정 비율 이상 떨어지면 발동하는 더 촘촘한 손절선이다. 레짐(하락장·횡보장·상승장)마다 이 하락폭 기준이 따로 설정돼 있었고, 대체로 기본 손절선보다 훨씬 타이트했다. 이론상 이게 제대로 작동했다면 6월의 손실은 훨씬 일찍, 훨씬 얕은 곳에서 끊겼어야 했다.

부검 — 진짜 절개 부위

트레일링 조건문 안쪽, 코드 맨 아래 블록에 조건문이 하나 더 숨어 있었다.

if peak_price > 0:
    peak_drop = (current_price - peak_price) / peak_price * 100
    if peak_drop <= -trailing_stop:
        if dynamic.get("use"):
            peak_pct = (peak_price - buy_price) / buy_price * 100
            t1 = float(dynamic.get("trigger1", 1.0))
            if peak_pct < t1:
                return None
        return f"트레일링({regime}) {peak_drop:.2f}%"
return None

하락폭 조건(peak_drop <= -trailing_stop)을 이미 만족했는데도, 매수가 대비 고점이 어떤 작은 기준선(t1)을 한 번도 넘은 적이 없으면 — 함수는 조용히 None을 반환한다. 신호가 발동 직전까지 갔다가, 마지막 줄에서 스스로 목을 조른 셈이다.

결함
트레일링 스탑은 원래 "고점에서 얼마나 떨어졌는가"만 보고 판단해야 하는 독립적인 안전장치다. 그런데 이 코드에서는 전혀 다른 지표 — "수익권 근처에 한 번이라도 진입했는가"라는 별개의 조건에 종속돼 있었다. 서로 다른 두 안전장치의 로직이 뒤섞이면서, 정작 트레일링이 가장 필요한 순간에 스스로 작동을 포기하도록 짜여 있었던 것이다.

쌍둥이 결함

실거래용 check_risk() 하나만 문제인 줄 알았다. 그런데 백테스트·가상청산 전용으로 따로 존재하는 check_risk_b_plan()을 열어보니, 토씨 하나 다르지 않은 같은 구조가 그대로 복사돼 있었다. 같은 로직을 실거래용과 검증용으로 각각 따로 작성해둔 탓에, 버그도 정확히 두 배로 복제돼 있었던 것이다.

검증용 코드까지 같은 결함을 갖고 있었다는 건, 그동안 돌려본 백테스트 결과조차 이 결함을 그대로 안고 나온 수치였다는 뜻이다. 지금까지 봐온 시뮬레이션 승률·손익 자체를 다시 의심해봐야 하는 이유가 여기서 하나 더 생겼다. 검증용 코드를 실거래 코드와 완전히 분리해서 짠 이유가 안전을 위해서였는데, 정작 그 분리가 같은 실수를 두 번 심는 통로가 돼버린 셈이다.

왜 하필 6월이었나

이 결함은 시장이 어느 방향으로든 뚜렷하게 움직일 때는 잘 드러나지 않는다. 매수 후 곧바로 수익권 근처까지 올라가는 장에서는 문제의 조건문 자체가 걸릴 일이 없기 때문이다. 문제는 매수 직후 오르지도 내리지도 못한 채 제자리에서 흔들리다가 서서히 미끄러지는 장 — 정확히 6월이 그랬다. 트레일링이 가장 필요한 장에서, 트레일링이 가장 먼저 무력화된 셈이다.

더 씁쓸한 건, 이 조건문을 처음 짤 때 분명 나름의 이유가 있었을 거라는 점이다. 아직 수익권 근처도 못 가본 포지션까지 트레일링으로 건드리면 너무 예민하게 반응하지 않을까 — 그런 걱정에서 나온 안전장치였을 가능성이 크다. 문제는 그 안전장치가 정반대 상황, 즉 정말로 손실이 벌어지는 순간에 오히려 방어선을 걷어내는 역할을 하고 있었다는 것이다. 의도와 결과가 이렇게까지 어긋날 수 있다는 걸, 이번에 다시 배웠다.

다음 편 예고

결함의 정체는 이제 완전히 드러났다. 8편에서 용의자를 추리고, 9편에서 범인을 확정하고, 10편에서 정확한 절개 부위까지 확인했다 — Part 3(부검)의 세 편이 이렇게 끝난다. 남은 건 실제로 메스를 대는 일이다. 트레일링 판단을 그 종속 조건에서 떼어내 독립된 안전장치로 되돌리고, 그 수정이 정말 효과가 있는지 지난 손실 구간을 다시 돌려보며 확인하는 일. 다음 편은 그 수술 기록이자, 이 시리즈의 마지막 두 편으로 넘어가는 다리이기도 하다.

이 시리즈는 실제 운용 중인 개인 자동매매 봇의 개발·운영 기록이며, 투자 조언이 아닙니다. 암호화폐 자동매매는 원금 손실 위험이 크며, 여기 소개된 전략·코드를 그대로 따라 하는 것을 권하지 않습니다.
댓글 0
첫 번째 댓글을 남겨보세요
댓글 작성
LOGIN