Mon 08-24 · 오늘 6건 · 통산 718호
falette
스크랩 쪽지 로그인
업비트 자동매매 봇 만들기
 
※ 이 게시글은 개인이 운영하는 자동매매 봇 개발 기록이며, 투자 조언이 아니라 개인 기록용입니다.

⑨편: 트레일링 스탑 — 범인 확정

peak_pct >= t1 (never true) buy_price return None trailing_stop 피크를 못 찍은 포지션은, 지켜지지 않는다

지난 편에서 용의자는 하나로 좁혀졌다. trailing_stop. 6/17~6/26 열흘 동안 63건, 합계 -44.68%. 다른 어떤 매도 사유보다 압도적으로 큰 손실이었다. 그런데 막상 코드를 열어보니, 예상했던 그림과는 좀 달랐다.

처음엔 부호가 잘못됐을 거라고 생각했다. 낙폭을 음수로 계산해놓고 조건식에서 실수했겠거니 — 실제로 그 줄만 따로 떼어놓고 보면 peak_drop <= -trailing_stop은 멀쩡한 비교식이다. 음수는 음수끼리 비교되고 있었다. 부호 버그는 없었다. 진짜 문제는 그 아래, 코드 세 줄에 숨어 있었다.

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          # 여기서 트레일링이 통째로 무효화된다

낙폭이 손절 기준을 넘어도, 그 앞에 조건이 하나 더 있었다. "매수 이후 최고가가 매수가 대비 t1%(기본 1%) 이상 올라본 적이 있는가." 없으면 return None — 손절 자체가 발동하지 않고 그냥 통과된다.

말이 되는 설계이긴 하다. 트레일링 스탑은 원래 "올랐다가 다시 빠질 때" 수익을 지키기 위한 장치니까. 한 번도 오른 적 없는 포지션에 "피크 대비 낙폭"을 적용하는 건 애초에 이상하다. 그래서 t1이라는 문턱을 걸어둔 거다. 문제는 이 문턱이 걸리는 조건이었다.

peak_price가 어떻게 초기화되는지 찾아봤다. 매수 체결 시점에, peak_price는 그 순간의 매수가로 그대로 시작한다. 그러니까 매수 직후 가격이 한 번도 위로 튀지 못하고 계속 눌리기만 하면, peak_pricebuy_price 근처에 머물고 peak_pct는 계속 t1 밑에 있게 된다.

숫자로 한 번 그려보면 이렇다. 어떤 코인을 100만 원에 매수했다고 하자. 체결 직후 peak_price도 100만 원으로 잡힌다. 이후 가격이 99만 원, 98만 원으로 계속 밀리기만 하고 단 한 번도 100만 원을 넘어서지 못했다면, peak_pct는 시종일관 0% 근처다. t1 기본값 1%를 넘을 일이 없다. 이 상태에서 가격이 97만 원까지 떨어져 낙폭이 트레일링 임계값(가령 -2.5%)을 넘어서도, 코드는 peak_pct < t1을 확인하고 조용히 None을 반환한다. 손절 신호 자체가 발생하지 않는다.

그리고 6월은 ⑧편에서 이미 확인했듯 bear 아니면 neutral, bull은 단 한 건도 없는 달이었다. BTC가 6/1 108,946,000원에서 6/23 93,972,000원까지 내내 미끄러진 그 구간이다. 이런 장에서 매수 직후 반등 한 번 없이 바로 눌리는 포지션이 흔했을 거라는 건 짐작이 아니라, 국면 자체가 그렇게 생겨먹었었다.

6월 누적 손익 곡선 (거래당 손익률 단순 합산)

0% -61% 6/1 6/16 -23.64% 6/23 -51.85% 6/26 -60.60% 6/30 -53.59%

6/16까지 완만하던 곡선이 6/17부터 열흘 만에 -60.60%까지 꺾인다. 이 구간(6/17~6/26)에 trailing_stop 63건, -44.68%가 몰려 있다. 말일 이틀 반등으로 -53.59%까지 일부 회복하며 6월을 마감했다.

이 곡선에서 가장 가파른 6/17~6/26 구간이 정확히 trailing_stop이 무력화됐던 구간과 겹친다는 게 우연이 아니다. 조정 국면에서 수익을 지키도록 설계된 장치가, 애초에 지킬 수익이 생기지도 않는 하락장에서는 그냥 잠들어 있었던 셈이다.

정리하면 이렇다. 트레일링 스탑은 "피크 대비 낙폭"을 감시하도록 설계됐는데, 그 감시가 작동하려면 먼저 피크가 있어야 한다. 그런데 하락장에서 매수한 포지션 상당수는 애초에 피크를 한 번도 못 만들어봤다. 감시 대상이 없으니 감시도 없었다. 코드가 부서진 게 아니라, 상승 후 조정을 관리하려고 만든 로직을 상승 자체가 없는 장에 그대로 얹어놓은 거였다.

남아있던 안전장치는 고정 stop_loss 하나뿐이었다. 국면별 stop_loss는 bear 1.5%, neutral 2.0% — 트레일링보다 더 타이트해 보이지만, 이건 매수가 대비 낙폭이지 피크 대비 낙폭이 아니다. 조금씩 오르내리며 서서히 밀리는 포지션은 고정 손절선에 닿기 전까지 계속 물려 있을 수 있다. 두 장치 사이에 낀 사각지대가 있었던 셈이다.

여기서 한 가지 더 짚을 게 있다. 이 로직은 애초에 "매수 후 한 번은 올랐다가 다시 빠지는" 그림을 전제로 짜여 있었다. 그런데 국면 판정 로직(④편)이 bear·neutral·bull을 나누면서도, trailing_stop의 t1 문턱 자체는 국면과 무관하게 고정값으로 걸려 있었다. 상승장 대응용 파라미터가 하락장에도 똑같이 적용된 거다. 국면별로 stop_loss·take_profit은 다르게 설정해놓고, 정작 이 무효화 조건의 기준은 국면을 타지 않았다.

결국 6월 열흘 동안의 손실은 어느 한 줄의 버그가 아니라, 설계 전제와 실제 시장 국면이 어긋난 결과에 가까웠다. "오른 뒤 떨어지는 상황"을 지키려던 로직이, "오른 적 없이 떨어지기만 하는 상황"에서는 그냥 침묵했다.

다음 편에서는 이 사각지대를 어떻게 메웠는지, 그리고 그 수정이 백테스트와 실거래에서 실제로 어떤 차이를 만들었는지를 본다.

본 게시글은 자동매매 봇의 실거래 로그를 바탕으로 한 운영 기록이며, 특정 종목이나 전략의 수익을 보장하지 않습니다. 암호화폐 투자는 원금 손실 위험이 있으며, 모든 투자 판단과 책임은 투자자 본인에게 있습니다.

지난 편: 용의자는 좁혀졌다. A, B, D를 지우고 남은 건 하나, trailing_stop이었다 — ⑧ 6월 -53% ① 용의자 지우기
다음 편: 사각지대를 메운 수정, 그리고 그 결과 — ⑩ 6월 -53% ③ (제목 미정)

개인 운영 기록이며 투자 권유가 아닙니다. 자동매매는 원금 손실 위험이 있고, 이 봇은 실제로 손실을 냈습니다. 전략 파라미터 일부는 구체적 수치 대신 상대적 표현으로 적었습니다.

댓글 0
첫 번째 댓글을 남겨보세요
댓글 작성
※ 이 게시판은 개인이 운영하는 자동매매 봇 개발 기록이며, 투자 조언이 아니라 개인 기록용입니다.
연재 화수 11
누적 조회 371
연재 기간 08.02 → 08.24
번호제목조회날짜
LOGIN