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

⑪ 계측 없는 봇은 고칠 수 없다

4부 · 정리

⑪ 계측 없는 봇은 고칠 수 없다

⑧편부터 ⑩편까지 석 달 가까이 같은 문장을 곱십었다. "왜 이걸 진작 못 봤을까." 트레일링 스탑을 죽이고 있던 조건문은 딱 한 줄이었다. peak_pct >= t1. 고치는 데는 십 분도 안 걸렸다. 그런데 그 한 줄을 찾아내기까지는 6월 한 달, 그리고 그 뒤로도 발행을 미루면서 두 달 가까이 걸렸다. 이번 편은 그 버그 얘기가 아니다. 왜 그렇게 오래 걸렸는지, 그 이유에 대한 얘기다.

로그는 있었는데, 로그가 답하지 못했다

봇은 처음부터 로그를 남기고 있었다. 매수했을 때, 매도했을 때, 가격과 시각과 사유 코드까지. coin_trades 테이블만 열어봐도 "언제 얼마에 샀고 언제 얼마에 팔았는지"는 완벽하게 나온다. 그래서 처음 손실이 이상하다는 걸 느꼈을 때도 별 걱정을 안 했다. "로그 보면 금방 나오겠지." 그런데 정작 열어보니 로그에 없는 게 문제였다. 트레일링 스탑이 "발동했다"는 기록은 있는데, "발동해야 했는데 안 됐다"는 기록은 아예 존재하지 않았다. 봇 입장에서는 그 조건을 평가했고, 조건이 거짓으로 나왔고, 그래서 아무 일도 일어나지 않은 거다. 아무 일도 안 일어난 건 로그에 남지 않는다. 이게 핵심이었다. 나는 "무슨 일이 일어났는지"는 기록하고 있었지만, "무슨 일이 일어날 뻔했다가 막혔는지"는 하나도 기록하지 않고 있었다.

처음엔 이게 로그 설계 문제라고만 생각했다. 그런데 곰곰이 따져보니 더 근본적인 문제였다. 매매 로그는 "결과"를 남기는 구조였지 "판단 과정"을 남기는 구조가 아니었다. check_risk()가 호출될 때마다 peak_pct, peak_drop, trigger1 세 값을 비교하는데, 그 비교 결과는 함수 안에서만 잠깐 존재하다가 사라진다. None을 리턴하고 끝나면 그 판단 자체가 흔적도 없이 증발한다. 몇 달 동안 하루에도 수십 번씩 이 함수가 조용히 트레일링 스탑을 막고 있었는데, 나는 그걸 볼 방법이 아예 없었다. 있는 줄도 몰랐던 게 아니라, 볼 수 있는 구조 자체가 없었던 거다.

결국 재구성으로만 확인할 수 있었다

그래서 결국 백테스트로 돌아갔다. 6월 한 달치 캔들을 다시 불러와서 같은 로직을 재현하고, 조건문 하나를 지웠을 때와 안 지웠을 때를 나란히 돌렸다. 트레일링 스탑 발동 횟수가 8건에서 121건으로 뛰었다. 같은 기간 손익은 -9.21%에서 -1.74%로 좁혀졌다. 숫자만 보면 통쾌한데, 이 과정 자체가 좀 부끄러웠다. 실거래 봇을 몇 달째 돌리면서, 정작 "이 조건이 왜 이렇게 결정했는지"를 알아내는 방법이 재현·재구성밖에 없었다는 뜻이니까. ④편에서 국면 분류 데이터를 뽑을 때도 비슷한 일이 있었다 — 그때도 캔들을 다시 그려서 92.5%짜리 확신으로 국면을 재구성해야 했다. 그게 그때는 "데이터가 부족해서"라고 넘겼는데, 지금 와서 보니 매번 같은 패턴이었다. 결정의 순간을 기록해두지 않으면, 나중엔 그 순간을 다시 만들어내는 수밖에 없다.

백테스트로 재구성한다는 게 말은 간단해 보이는데, 실제로 해보면 손이 많이 간다. 캔들 데이터를 다시 받아오고, 그때 시점의 파라미터 값이 지금과 같은지부터 확인해야 하고(중간에 값을 손으로 바꾼 적이 있었으면 재구성이 통째로 틀어진다), 봇 코드와 백테스트 코드가 실제로 같은 로직을 타는지도 한 줄씩 대조해야 한다. 이 작업만 며칠이 걸렸다. 그런데 이게 원래는 필요 없는 작업이었다. check_risk() 안에서 트리거를 평가할 때마다 그 시점의 peak_pct, peak_drop, 판단 결과를 한 줄씩만 파일에 남겼어도, 6월 데이터를 뒤늦게 다시 돌려볼 필요 없이 그냥 로그 파일을 grep 한 번 하면 끝났을 일이다. 계측 하나를 안 붙여둔 대가로, 나중에 며칠짜리 고고학 발굴을 하게 된 셈이다.

이 버릇은 코드 밖에서도 똑같았다

⑨편을 쓰면서 알게 된 사실 하나도 결이 같다. 그 편은 falette.org가 아니라 코인랩에 발행돼 있었다. 몇 주 동안 falette.org 게시판에서 ⑨편이 안 보인다는 걸 눈치도 못 챘다. 발행 버튼을 누른 기록은 어딘가에 있었을 텐데, "이 글이 어느 사이트에 최종적으로 올라갔는지"를 확인하는 절차가 따로 없었다. 봇의 트레일링 스탑이나, 글 발행 사이트나 — 결국 같은 구멍이었다. "실행했다"와 "실행한 결과가 내가 원하는 곳에 정확히 반영됐다" 사이에 확인 단계가 하나 빠져 있었던 거다. falette.org와 코인랩이 완전히 별개 사이트라는 걸 이미 알고 있었는데도 이런 일이 생겼다는 게, 계측 부재라는 게 특정 코드 한 줄의 문제가 아니라 운영 습관 전체의 문제라는 걸 다시 확인시켜줬다.

돌이켜보면 crontab suid 비트가 재부팅마다 풀리는 문제도 비슷한 결이었다. 매번 "왜 워치독이 안 도는거지" 하고 한참 헤매다가 결국 chmod u+s를 다시 걸어주는 식으로 넘어갔는데, 정작 "재부팅 후 suid 비트가 풀렸는지"를 확인하는 절차를 만들어둔 적은 없다. 매번 사람이 감으로 눈치채고 수동으로 고쳤다. NAS 하나 운영하면서 이런 식으로 감에 의존해 넘어간 지점이 한둘이 아니었다는 걸, 이번 편을 쓰려고 지난 기록들을 쭉 훑어보면서 새삼 깨달았다.

지금 다시 확인해보니

이 편을 쓰면서 실제로 뒤져봤다. 결과는 예상보다 더 심했다. 최근 사흘치 로그를 통째로 뒤져도 trailing이나 peak_pct나 trigger 관련 기록은 단 한 줄도 없었다. 그런데 봇 코드 안에는 이미 제대로 된 로거가 있다. KST 시간대 포맷터까지 따로 만들어서 info·warning·error 세 단계로 남기게 짜놓은 로거다. 문제는 이 로거가 check_risk()와 check_risk_b_plan() 안에서는 단 한 번도 호출되지 않는다는 거다. 도구는 이미 손에 쥐고 있었는데, 정작 가장 중요한 판단이 일어나는 그 지점에는 붙일 생각을 못 했다. "계측이 없다"는 게 기술이 부족해서가 아니라, 있는 도구를 어디에 써야 하는지 놓친 문제였다는 걸 이번에 확인하고 나서야 인정하게 됐다.

peak_pct 값 자체는 다른 자리에서 살아있긴 하다. 상태 저장용 코드 어딘가에서 현재 peak_pct를 계산해 coin_status에 스냅샷으로 얹어놓는다. 그런데 이건 "지금 이 순간 peak_pct가 얼마다"는 사진 한 장이지, "그 트리거를 평가했더니 통과였는지 차단이었는지"는 안 남는다. 사진은 있는데 그 사진이 어떤 판단으로 이어졌는지에 대한 설명이 없는 셈이다. 메모리에 live_monitor.py라는 파일이 있다고 적어뒀었는데, 이번에 찾아보니 실제로는 존재하지 않는 파일이었다. 이것도 결국 같은 얘기다 — 확인 안 하고 있는 줄 알고 넘어간 것 자체가, 이 편에서 계속 얘기하는 "계측 부재"의 또 다른 얼굴이다.

다음 편 — 그래서 뭘 버릴 것인가

⑫편에서는 반대 얘기를 하려고 한다. 계측을 더 붙이는 얘기 말고, 지금 있는 것 중에 뭘 버릴지에 대한 얘기. 다섯 개 매수 관문, 3단 손절, 국면 분류, 웹 명령 인터페이스 — 여기까지 열두 편 가까이 쌓아온 구조 중에서, 실제로 승패에 영향을 준 건 몇 개나 되고 그냥 안심하려고 넣어둔 건 몇 개나 되는지. 이번 편에서 다룬 "계측"이 바로 그 판단의 재료가 될 거다. 재는 게 없으면 버릴 것도 못 정하니까.

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