Mon 08-03 · 오늘 0건 · 통산 1,339호
falette
스크랩 쪽지 로그인
업비트 자동매매 봇 만들기
 

① 업비트 자동매매 봇 만들기 — 113일, 369번의 매도, 그리고 하루에 잃은 16%

4개월을 돌렸다. 승률 45%, 손익비 0.76. 6월 23일 하루에만 22번 팔아서 거래 손익 합계 −16.54%를 찍었다. 원인은 시장이 아니었고, 서버 장애도 아니었고, 종목 선정도 아니었다. 매도 코드 안의 조건문 하나였다.

운영
113일
매도
369건
승률
45.3%
손익비
0.76

자동매매 글을 찾아보면 대개 둘 중 하나다. 수익 인증이거나, "이렇게 만들면 됩니다" 하는 튜토리얼이거나. 정작 궁금한 건 돌려본 뒤에 무엇이 어떻게 틀어졌는가인데 딱 그 부분이 없다. 잘 안 됐다는 얘기는 아무도 안 쓰기 때문이다.

이 시리즈는 그 자리를 메운다. 4개월간 실제로 돌아간 봇의 설계를 열어보고, 남은 거래 기록으로 판단 하나하나를 채점한다. 그리고 6월에 벌어진 일을 부검한다. 결론부터 말하면 원인은 매도 로직에 뚫려 있던 구멍 하나였고, 그걸 찾는 데 넉 달이 걸렸다.

시리즈 지도
1부 · 설계
  1. ① 113일이 남긴 숫자와 이 시리즈의 질문 — 지금 글
  2. ② 매수 로직 해부 — 다섯 개의 관문
  3. ③ 매도 로직 해부 — 손절·익절·트레일링의 3층 구조
  4. ④ 시장 국면을 셋으로 나눈다는 것
2부 · 운영
  1. ⑤ NAS에서 24시간 — 부팅 스크립트가 조용히 실패하던 이유
  2. ⑥ 워치독과 중복 프로세스 — 봇이 두 개 떴을 때
  3. ⑦ 웹에서 봇에게 명령 보내기
3부 · 부검
  1. ⑧ 6월 −53% ① 용의자 지우기
  2. ⑨ 6월 −53% ② 진범
  3. ⑩ 패치 이후 — 승률이 두 배 올랐는데 손익비는 반토막
4부 · 정리
  1. ⑪ 계측 없는 봇은 고칠 수 없다
  2. ⑫ 다음 버전 — 무엇을 버릴 것인가

왜 직접 만들었나

자동매매 서비스는 이미 여러 개 있다. 안 쓴 이유는 셋이었다.

첫째, 남의 봇은 왜 샀는지 알 수 없다. 손실이 나도 전략이 틀린 건지 구현이 틀린 건지 구분할 방법이 없다. 원인을 모르면 고칠 수도 없다. 이 시리즈에서 다룰 가장 큰 실수도 결국 전략 문제가 아니라 구현 문제였다. 남의 봇이었다면 영영 못 찾았을 것이다.

둘째, 구독료가 수익률의 허들이 된다. 월 구독료를 넘는 수익을 못 내면 그 자체로 마이너스다. 결과적으로 이 봇도 수익을 못 냈지만, 최소한 고정 지출은 없었다.

셋째, 거래소 API 키를 남에게 넘기는 게 내키지 않았다. 출금 권한을 빼고 발급하더라도 내 자산을 움직이는 권한이 통제 밖에 있는 상태는 편하지 않았다.

무엇을 만들었나

파일 여섯 개, 프로세스 둘

구조는 단순하다. 설정 파일 하나, 봇 본체 하나, 그걸 지켜보는 감시 스크립트 하나. 상태를 밖에서 볼 수 있게 웹 대시보드를 붙였고, 반대로 웹에서 봇에게 명령을 넣는 통로도 하나 냈다.

업비트 API 텔레그램 coin_config.json watchdog.sh coin_trades.db 웹 대시보드 시세 · 캔들 · 주문 체결 · 오류 알림 모든 파라미터 60초마다 생존 확인 거래 기록 · SQLite 상태 조회 · 수동 명령 지표 계산 → 매매 판단 → 주문 → 기록 30초 루프 · 설정 핫리로드 조회 coin_bot.py
30초에 한 바퀴. 한 바퀴 안에서 감시 종목 전체의 지표를 다시 계산하고, 조건에 맞으면 주문을 내고, 결과를 DB와 텔레그램에 남긴다.

설계에서 특히 신경 쓴 건 설정을 바꾸려고 봇을 죽이지 않아도 되게 만드는 것이었다. 루프마다 설정 파일을 다시 읽는다. 파라미터 하나 고치겠다고 프로세스를 재시작하면 그 순간 보유 포지션의 손절 감시가 끊긴다. 초기에 그렇게 하다가 손절 타이밍을 놓친 적이 있어서 핫리로드를 넣었다.

지금 보면 잘못한 것 봇 본체를 파일 하나에 다 넣었다. 지표 계산, 전략 판단, 주문 실행, DB 기록, 알림 발송이 전부 한 곳에 있다. 처음엔 셋으로 나눌 생각이었는데 상태를 공유하는 지점이 자꾸 생기면서 합쳐졌고, 결국 84KB짜리 단일 파일이 됐다. 나중에 "이 손실이 전략 탓인가 실행 탓인가"를 가려야 할 때 이 구조가 정확히 발목을 잡았다.

지표는 두 개만

지표를 늘리고 싶은 유혹이 계속 있었다. MACD, 스토캐스틱, 일목균형표까지 얹으면 정교해 보인다. 안 한 이유는 검증할 수 없는 전략은 고칠 수도 없기 때문이다. 지표가 여섯 개면 손실이 났을 때 어느 지표가 잘못 판단했는지 특정할 수 없다.

그래서 역할을 나눠 둘만 썼다. RSI는 매수 판단 전담, 볼린저밴드는 매도 판단 전담. 이동평균은 매매 신호가 아니라 시장 국면 판정에만 썼다. RSI 기간은 교과서적인 14 대신 한 자릿수로 줄였고, 과매도 기준선은 관례인 30보다 조금 위로 올렸다. 짧은 봉을 쓰면서 느린 지표를 쓰는 게 앞뒤가 안 맞았고, 30을 고집하면 신호가 몇 주에 한 번 뜨기 때문이다.

둘 다 "더 자주, 덜 확실한 자리에서도 사겠다"는 선택이었다. 이 트레이드오프에서 내가 어느 쪽에 섰는지는 아래 숫자가 말해준다. 구체적인 조건 조합은 ②편에서 따로 뜯는다.

113일 성적표

2026년 4월 9일부터 7월 31일까지. 매수 365건, 매도 369건. 전체 승률 45.26%.

매도승률평균수익평균손실손익비
057047.1%+0.68%−1.02%0.66
0620036.0%+0.82%−0.88%0.93
077675.0%+0.58%−1.45%0.40

이 표를 처음 뽑았을 때 한참을 들여다봤다. 뭔가 이상하다.

5월6월7월 47.1%36.0%75.0% 승률 0.660.930.40 손익비
두 선이 정확히 반대로 움직인다. 승률이 가장 낮았던 6월에 손익비는 가장 좋았고, 승률이 75%까지 뛴 7월에 손익비는 0.4로 주저앉았다.

보통 이렇게 안 움직인다. 승률과 손익비는 대개 같은 방향으로 개선되거나, 최소한 무관하게 움직인다. 정확히 반대로 움직였다는 건 두 달 사이에 청산 방식 자체가 바뀌었다는 뜻이다.

실제로 그랬다. 그리고 그 변화는 시장이 만든 게 아니라 내가 만든 것이었다.

6월에 무슨 일이 있었나

일자별로 펼치면 이렇게 생겼다.

+5%0−10%−17% 6/29 패치 010815 202330 −16.54%
파란색이 손실일, 빨간색이 수익일. 6월 23일 하루에 22번 팔아서 −16.54%. 그리고 6월 29일 코드를 한 줄 고친 시점을 경계로 그래프가 뒤집힌다.

6월 23일은 매수 21건, 매도 22건. 그날 하루 손익 합계가 −16.54%다. 6월 전체 손실의 3분의 1이 하루에 나왔다.

그리고 그래프 오른쪽 끝, 6월 29일 이후 이틀이 눈에 띈다. 갑자기 플러스로 돌아선다. 그날 오전 9시 5분에 매도 로직 한 곳에 조건을 하나 추가했고, 그 전후 성적이 이렇게 갈렸다.

구간기간매도승률평균손익
패치 전 (6/24~6/29)4.3일4330.2%−0.21%
패치 후 (6/29~6/30)1.6일1984.2%+0.38%

조건문 하나에 승률이 30%에서 84%로 움직였다. 표본이 작아서 이것만으로 단정할 수는 없지만, 7월 한 달치 데이터가 같은 방향을 가리킨다.

먼저 지운 용의자들

아닌 것부터 확인해야 맞는 걸 찾는다

원인을 찾을 때 제일 먼저 한 건 가능성 있는 설명을 하나씩 지우는 일이었다. 이 과정이 ⑧편의 내용인데, 결과만 먼저 요약한다.

서버 장애가 아니었다

봇은 NAS에서 돌아가고, NAS는 가끔 말썽을 부린다. 그래서 6월 손실 상위 5일의 로그를 전부 열어봤다.

날짜손익에러봇 재시작중복 프로세스
06-23−16.54%010
06-01−10.65%040
06-04−8.60%000
06-17−6.19%020
06-25−5.79%000

에러 로그 0건, 예외 발생 0건, 재시도 소진 0건, 워치독이 중복 프로세스를 강제 종료한 흔적도 0건. 재시작 기록은 있지만 크래시 흔적 없이 이뤄진 것들이라 배포나 수동 재기동으로 보인다. 봇은 아주 건강하게, 아주 성실하게 돈을 잃고 있었다.

특정 종목 문제도 아니었다

손실 상위 5일의 매도를 종목별로 세어보니 SOL 17건, ETH 15건, BTC 9건, XRP 8건, NEAR 6건, SUI 4건, DOGE 4건. 감시 종목 전체에 고르게 퍼져 있다. 특정 코인이 이상하게 움직여서 물린 게 아니라, 어느 종목을 사도 같은 방식으로 잃고 있었다는 뜻이다. 이건 종목 문제가 아니라 로직 문제라는 신호다.

포지션을 과하게 잡은 것도 아니었다

동시 보유 종목 수를 시점별로 복원해봤다.

평균 동시보유최대
051.614
061.424
071.254

가장 크게 잃은 6월이 5월보다 오히려 들고 있었다. 보유 한도(4종목)에 도달한 시점도 전체 734건 중 20건, 2.7%뿐이라 한도가 병목이 된 적도 없다. 물량을 과하게 잡아서 터진 게 아니다.

다만 확인된 것 하나 1회 주문 규모는 4월 대비 6월에 5.6배까지 커져 있었다. 설정을 바꾼 적은 없다. 주문액이 총자산 비율로 잡히는 구조라, 자산이 불어나는 만큼 베팅 사이즈도 자동으로 커진 것이다. 이게 손실의 원인은 아니지만 손실의 절대 크기를 키운 증폭기 역할은 했다. 그리고 나는 이 봇이 고정 금액으로 주문하는 줄 알고 있었다. 설정을 만든 사람이 자기 설정을 잘못 알고 있었던 셈이다.

남은 건 매도 로직뿐이었다

세 용의자를 지우고 나니 남은 곳은 하나였다. 그래서 왜 팔았는지를 월별로 세어봤다. 이 표 하나가 사실상 답이었다.

종료 사유5월6월7월
트레일링 스탑1.4%55.5%0.0%
동적 손절 — 본전0.0%13.0%53.9%
고정 손절8.6%8.5%25.0%
볼린저 상단31.4%11.0%15.8%
고정 익절0.0%4.5%5.3%
동적 손절 — 수익보호1.4%0.0%0.0%
기록 없음 (구버전)57.1%7.5%0.0%

맨 윗줄을 보자. 1.4% → 55.5% → 0.0%. 6월 매도의 절반 이상이 트레일링 스탑이었고, 7월엔 한 건도 없다.

그리고 6월 트레일링 청산 구간의 성적은 이랬다. 승률 0%. 평균 −0.685%. 트레일링으로 나간 거래 중 이익으로 끝난 게 하나도 없었다.

승률이 0%인 청산 방식이 전체 매도의 절반을 차지하고 있었다는 뜻이다. 6월에 −53%가 나온 건 시장이 나빠서가 아니라, 손실을 확정하는 기계가 하루 일곱 번씩 돌아가고 있었기 때문이다.

왜 이런 일이 벌어졌나 트레일링 스탑은 원래 이익이 난 포지션의 이익을 지키는 장치다. 최고점 대비 얼마 이상 밀리면 나가는 방식인데, 여기에 "먼저 이익이 나 있어야 한다"는 전제 조건이 빠져 있었다. 그래서 매수 직후 가격이 조금만 흔들려도 트레일링이 발동했고, 그건 구조적으로 항상 손실이었다. 6월 29일에 추가한 조건문 하나가 바로 이 전제를 되살린 것이다. 어떻게 이런 코드가 나왔고 왜 넉 달이나 못 봤는지는 ⑨편에서 코드와 함께 다룬다.

그런데 고치고 나서도 이상하다

패치 후 7월 승률은 75%로 뛰었다. 그런데 같은 달 손익비는 0.40으로 시리즈 최악이었다. 평균 손실이 −1.45%까지 벌어졌기 때문이다.

이유는 짐작이 간다. 예전엔 손실 포지션이 트레일링에 걸려 −0.7% 언저리에서 잘려 나갔다. 그 장치를 막았더니 이제 지는 거래가 고정 손절선까지 끝까지 간다. 지는 횟수는 줄었지만, 한 번 질 때 더 크게 진다.

그럼 이건 개선인가 아닌가. 순손익만 보면 개선이다. 6월 −53.59%에서 7월 +5.33%로 돌아섰으니까. 하지만 손익비 0.40은 승률 71% 이상을 계속 유지해야만 본전이라는 뜻이고, 그건 지속 가능한 조건이 아니다. 한 달 반짝 나온 승률에 기대는 구조다.

즉 하나를 고쳤더니 다른 게 드러난 상태다. 이 이야기가 ⑩편이다.

이 시리즈가 답하려는 질문

여기까지가 지도다. 앞으로 열한 편에 걸쳐 아래 질문에 하나씩 답한다.

  1. 01다섯 개의 매수 조건은 각각 무엇을 막으려고 만들었고, 실제로 막았는가
  2. 02이익이 난 적 없는 포지션에 트레일링 스탑이 걸리는 코드는 어떻게 태어나는가
  3. 03왜 넉 달 동안 그걸 못 봤는가 — 계측 설계의 실패
  4. 04승률과 손익비 중 무엇을 먼저 고쳐야 하는가
  5. 05시장 국면을 코드로 판정한다는 게 가능한 일인가
  6. 06이 봇을 계속 돌릴 것인가, 접을 것인가

마지막 질문에 대한 답은 아직 안 정했다. ⑫편을 쓸 때쯤이면 정해져 있을 것이다.

먼저 짚어둘 것

이 글에 나오는 월별·일별 수치는 각 거래 수익률의 단순 합계이지 계좌 수익률이 아니다. 주문액이 총자산 비율로 잡히기 때문에 실제 잔고 변화와는 다르게 움직인다. 계좌 기준 실수익률은 ⑫편 결산에서 따로 계산한다. 실제보다 커 보일 수도 작아 보일 수도 있어서 미리 밝혀둔다.

그리고 5월 데이터는 신뢰도가 낮다. 당시 버전엔 종료 사유 로깅이 없어서 5월 매도의 57%가 왜 팔렸는지 기록이 없다. 4개월치 데이터 중 상당 부분을 원인 분석에 쓸 수 없다는 뜻이고, 이건 전략이 아니라 계측 설계의 실패다. "일단 돌려보고 로그는 나중에" 라고 생각했던 대가를 정확히 치렀다.

가장 크게 배운 것 매매 로직보다 기록 설계를 먼저 했어야 했다. 왜 샀고 왜 팔았는지, 그 순간 지표가 얼마였는지를 남기지 않은 거래는 나중에 어떤 분석도 할 수 없는 죽은 데이터가 된다. 이 시리즈에서 6월의 원인을 찾을 수 있었던 건 순전히 6월에는 그 로그가 있었기 때문이다.

다음 편

②편에서는 매수 로직을 뜯는다. 다섯 개 조건을 AND로 직렬 연결한 구조, 그중 "지금 과매도인가"가 아니라 "최근에 과매도였는가"를 묻게 된 이유, 그리고 그 조건들이 만들어낸 지나치게 좁은 진입 창에 대해. 평균 수익 +0.72%라는 초라한 숫자가 어디서 나왔는지가 거기 있다.

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

거래소 API 키를 코드나 설정 파일에 평문으로 저장하지 마세요. 이 봇의 초기 버전이 그랬고, 지금도 정리 중입니다. ⑤편에서 함께 다룹니다.

댓글 0
첫 번째 댓글을 남겨보세요
댓글 작성
LOGIN