매매 로그 — 봇이 왜 그 주문을 냈는지 나중에 알아내는 법
봇이 밤사이 이상한 주문을 냈습니다. 아침에 열어 보니 손실이 나 있고, 왜 그 자리에서 들어갔는지 알 수가 없습니다. 이때 할 수 있는 일은 로그에 무엇이 적혀 있느냐로 정해집니다. 적혀 있지 않은 것은 영원히 알 수 없습니다. 그 순간의 시세도 계좌 상태도 이미 지나갔기 때문입니다.
로그는 사후에 만들 수 없다
코드를 다시 읽으면 되지 않느냐고 생각하기 쉽습니다. 되지 않습니다. 코드는 "어떤 조건이면 들어간다"를 말해 줄 뿐이고, 우리가 알아야 하는 것은 그 순간 그 조건에 들어간 값이 얼마였느냐입니다. 지표가 얼마였는지, 가용 증거금이 얼마였는지, 거래소가 뭐라고 답했는지는 코드 어디에도 없습니다.
다시 돌려보면 되지 않느냐도 마찬가지입니다. 재현하려면 그 시점의 호가와 계좌 상태가 있어야 하는데, 둘 다 남아 있지 않습니다. 봉 데이터는 나중에 받을 수 있지만 호가는 못 받습니다. 그래서 로그는 사고가 난 뒤에 만들 수 없는 유일한 자료입니다. 미리 남겨 두지 않았으면 그걸로 끝입니다.
그런데 대부분의 봇 로그는 이렇게 생겼습니다. "진입 BTCUSDT LONG 0.1". 이 한 줄로는 아무것도 못 합니다. 왜 그때였는지, 왜 0.1이었는지, 왜 롱이었는지가 전부 빠져 있습니다.
한 건을 재구성하려면 일곱 가지가 필요하다
거래 한 건을 나중에 그대로 되짚으려면 아래 일곱 가지가 한 줄 안에 같이 있어야 합니다. 흩어져 있으면 맞춰 보는 데만 하루가 갑니다.
① 결정 ID
이 결정과 관련된 모든 줄에 같은 값
② 시각 3종
결정 · 전송 · 체결 (밀리초까지, UTC)
③ 주문 내용
종목 · 방향 · 수량 · 주문가 · 주문 유형
④ 결정 근거 값
조건식에 들어간 지표·가격 숫자 그대로
⑤ 계좌 상태
가용 증거금 · 보유 포지션 · 미체결 주문 수
⑥ 거래소 응답 원문
주문 ID · 상태 · 에러 코드
⑦ 코드 버전
커밋 해시 7자리
─────────────
①이 없으면 나머지를 이어 붙일 수 없다
①이 제일 먼저인 이유가 있습니다. 한 번의 결정이 여러 줄을 만들기 때문입니다. 판정 한 줄, 주문 전송 한 줄, 응답 한 줄, 체결 통보 한 줄. 같은 결정 ID가 박혀 있어야 이 넷을 묶을 수 있습니다. 시각으로 맞추려 하면 종목이 여러 개일 때 금방 섞입니다.
⑦도 빼먹기 쉬운데 실제로는 아주 자주 쓰입니다. 일주일 전 사고를 조사할 때 지금 코드를 읽으면 안 되기 때문입니다. 그 사이에 고친 게 있으면 엉뚱한 곳을 봅니다. 커밋 해시 한 줄이면 그때 코드를 정확히 꺼내 볼 수 있습니다.
시각은 하나가 아니라 세 개다
"주문 시각"을 하나만 적는 봇이 많습니다. 그러면 느린 구간을 못 찾습니다. 결정에서 체결까지는 최소 세 구간으로 나뉘고, 각 구간이 느려지는 원인이 서로 다릅니다.
09:30:00.120 결정 기준가 $100.00
09:30:00.185 전송 +65ms
09:30:00.240 접수 +55ms
09:30:00.640 체결 +400ms 체결가 $100.07
결정 → 체결
520ms
가격이 밀린 폭
$0.07 ÷ $100.00 = 0.07%
노셔널 $800 기준
슬리피지 $800 × 0.07% = $0.56
왕복 수수료 $800 × 0.10% = $0.80
합 $1.36 (노셔널의 0.17%)
─────────────
시각이 하나면 이 520ms가 통으로 보인다
세 구간을 나눠 놓으면 원인이 바로 드러납니다. 결정에서 전송까지가 길면 우리 코드가 느린 것이고, 전송에서 접수까지가 길면 네트워크나 요청 한도 문제이고, 접수에서 체결까지가 길면 호가가 얇았던 것입니다. 통으로 520ms만 적혀 있으면 셋 중 무엇을 고쳐야 할지 알 수 없습니다.
시각은 UTC로, 밀리초까지 적습니다. 거래소가 주는 시각이 UTC라서 로컬 시각으로만 적으면 대조할 때마다 변환해야 하고, 그 변환이 틀리면 조사 전체가 틀어집니다. 초 단위까지만 적는 것도 곤란합니다. 위 예시가 통째로 같은 초 안에서 일어납니다. 시각 자체가 어긋나는 문제는 타임스탬프 오류 쪽에 따로 정리돼 있습니다.
슬리피지가 실제로 얼마였는지도 이 세 시각이 있어야 계산됩니다. 결정 시점 기준가와 체결가가 둘 다 있어야 차이를 구할 수 있는데, 체결가만 적어 두면 비교 대상이 없습니다. 슬리피지를 줄이려는 시도가 효과가 있었는지조차 측정이 안 됩니다.
진입하지 않은 신호도 적는다
여기서 많이들 갈립니다. 대부분은 주문을 낸 것만 기록합니다. 그러면 필터가 일을 잘하는지 영원히 알 수 없습니다.
조건이 열 개인 전략에서 주문이 안 나갔을 때, 열 개 중 어느 것이 막았는지가 안 적혀 있으면 그 필터는 검증 대상이 아니라 그냥 믿는 대상이 됩니다. 실제로 필요한 것은 "거부됨"이 아니라 "거부한 조건 이름과 그때의 값"입니다.
거부 사유 변동성 필터
기준 RMS ≥ 0.20%
실제값 RMS = 0.14%
통과한 조건도 값을 같이
거래대금 $41,200 (기준 $20,000)
보유 종목 12 (상한 40)
─────────────
"막혔다"가 아니라 "0.14라서 막혔다"
이렇게 적어 두면 나중에 기준을 0.20에서 0.15로 바꿨을 때 어떤 신호가 추가로 들어왔을지를 로그만으로 세어 볼 수 있습니다. 적어 두지 않았으면 다시 백테스트를 돌려야 하고, 그 백테스트가 실전과 같은 조건이라는 보장은 또 없습니다.
전부 적으면 용량이 감당되지 않는다
그렇다고 매 사이클 전 종목을 다 적으면 금방 벽에 부딪힙니다. 숫자로 보면 분명합니다.
사이클 주기 5초
하루 86,400 ÷ 5 = 17,280 사이클
종목 40개
17,280 × 40 = 691,200줄/일
한 줄 400바이트
691,200 × 400 = 264MB/일
30일 7.7GB
90일 23GB
─────────────
필요할 때 열어 볼 수 없는 크기가 된다
문제는 디스크만이 아닙니다. 23GB에서 특정 순간을 찾는 일 자체가 어려워집니다. 그래서 전수 기록 대신 상태가 바뀌는 순간만 남깁니다.
하루 진입 20건 × 6줄
(판정·전송·응답·체결·청산판정·청산체결)
= 120줄
거부·오류 50줄
5분마다 요약 288줄
(보유·증거금·연결 상태)
합 458줄/일
458 × 400B = 0.17MB/일
90일 약 16MB
─────────────
23GB가 16MB가 된다
5분 요약을 넣는 이유는 아무 일도 없던 구간과 봇이 죽어 있던 구간을 구분하기 위해서입니다. 이 줄이 없으면 로그가 조용한 것이 정상인지 사고인지 알 수 없습니다. 봇 재시작 뒤 몇 시간이 비어 있는 것을 나중에 발견하는 일이 여기서 갈립니다.
형식은 한 줄에 하나의 JSON을 넣는 방식이 편합니다. 사람이 읽을 수 있으면서 도구로 걸러 내기도 쉽습니다. 파일은 날짜별로 나누고, 프로세스마다 다른 파일에 씁니다. 여러 프로세스가 한 파일에 동시에 쓰면 줄이 중간에 섞여서 그 줄을 통째로 못 읽게 됩니다.
절대 적으면 안 되는 것
응답을 통째로 찍는 코드가 흔한데, 여기서 사고가 납니다. 요청 객체에는 API 키와 서명이 헤더에 들어 있습니다. 오류가 났을 때 요청 전체를 찍는 습관이 있으면 그 순간 로그 파일이 자격 증명 파일이 됩니다.
× API 키 · 시크릿 · 패스프레이즈
× 요청 서명 값
× 인증 헤더 전체
× 계좌 고유 번호 · 개인 식별 정보
대신
○ 키는 앞 4자리만 (kr_a1b2…)
○ 응답은 필요한 필드만 골라서
○ 에러는 코드와 메시지만
─────────────
로그 파일은 백업·공유되기 쉽다
로그는 조사하려고 만드는 것이라 다른 사람에게 보내거나 클라우드에 올리게 됩니다. 그때 키가 같이 갑니다. 처음부터 안 적는 것 말고는 방법이 없습니다. 거래소가 돌려준 거부 코드와 메시지만 있으면 원인 파악에는 충분합니다.
로그가 있으면 할 수 있게 되는 것
로그를 제대로 남기면 사고 조사 말고도 쓸 데가 생깁니다. 결정 시점 기준가와 체결가가 둘 다 있으면 슬리피지 통계를 낼 수 있고, 진입과 청산 사이의 기록이 있으면 MFE·MAE를 계산해 손절과 익절 자리가 맞는지 볼 수 있습니다.
거래소가 알려 주는 포지션과 봇이 알고 있는 포지션이 어긋났을 때도, 어느 주문부터 어긋났는지를 로그로 거슬러 올라가 찾습니다. 포지션 대조가 차이를 알려 준다면 로그는 그 차이가 언제 생겼는지를 알려 줍니다. 같은 주문이 두 번 나간 사고도 결정 ID가 박혀 있으면 중복 주문인지 별개 주문인지 바로 갈립니다.
지표가 덜 채워진 채로 낸 주문인지도 로그로만 확인됩니다. 결정 근거 값에 그때 쓴 봉 개수를 같이 적어 두면 지표 워밍업이 끝나기 전에 나간 주문을 나중에 골라낼 수 있습니다.
정리
② 코드를 다시 읽어도 그때의 값은 안 나온다
③ 호가와 계좌 상태는 나중에 못 구한다
④ 한 건 재구성에는 일곱 항목이 필요하다
⑤ 결정 ID가 없으면 줄들을 이어 붙일 수 없다
⑥ 커밋 해시가 있어야 그때 코드를 볼 수 있다
⑦ 시각은 결정·전송·체결 세 개를 따로 적는다
⑧ 520ms를 통으로 적으면 느린 구간을 못 찾는다
⑨ 시각은 UTC로 밀리초까지 적는다
⑩ 기준가가 없으면 슬리피지를 계산할 수 없다
⑪ 거부된 신호도 값과 함께 적는다
⑫ 5초 40종목 전수 기록은 90일에 23GB다
⑬ 상태 변화만 남기면 90일 16MB가 된다
⑭ 5분 요약으로 무소식과 사망을 구분한다
⑮ API 키와 서명은 어떤 경우에도 적지 않는다
한 줄로 줄이면 이렇습니다. 로그에 안 적은 것은 일어나지 않은 것과 같습니다. 무엇을 적을지는 사고가 나기 전에 정해 두는 수밖에 없습니다.
주의
본문의 결정 09:30:00.120부터 체결 09:30:00.640까지의 520ms 분할과 65ms·55ms·400ms, 기준가 $100.00 대비 체결가 $100.07과 0.07%, 노셔널 $800 기준 슬리피지 $0.56·왕복 수수료 $0.80·합 $1.36과 0.17%, 사이클 주기 5초와 하루 17,280 사이클, 종목 40개와 691,200줄, 한 줄 400바이트와 264MB·7.7GB·23GB, 상태 변화 기록의 하루 20건·6줄·120줄·거부 50줄·요약 288줄·합 458줄과 0.17MB·16MB, 변동성 필터 기준 RMS 0.20%와 실제값 0.14% 같은 수치는 로그 설계의 계산 구조를 보여주기 위한 가정 예시이며 실측이 아닙니다. 한 줄 400바이트는 남기는 필드 수에 따라 크게 달라지고, 네트워크 지연과 체결 지연은 거래소·회선·시간대·호가 두께에 따라 달라집니다. 권장 보존 기간과 요약 주기도 임의로 정한 값이므로 쓰는 전략의 매매 빈도에 맞춰 다시 잡아야 합니다. 로그를 잘 남긴다고 해서 전략이 수익을 내지는 않습니다. 이 글은 자동매매 구현과 운영 기록에 대한 설명이며 특정 종목·방향이나 진입·청산 시점을 권유하지 않습니다. 암호화폐 거래는 원금 전액을 잃을 수 있고 레버리지를 쓰면 손실이 증거금을 넘을 수도 있습니다. 투자 판단과 그 결과는 본인 책임입니다.
NOONOO TRADING 무료 채팅방에서 실시간 트레이딩을 같이 보세요.
봇에서 시작하기📈 OKX 신규 가입 시 거래 수수료 할인
OKX 수수료 할인 가입 →