라이브를 운영하다 보면 손실 자체보다 더 당황스러운 순간이 있다. 이번엔 그게 숫자였다. 텔레그램과 로그에는 그저 보통 한 번의 손절처럼 보였는데, 실제 거래소 계좌에서 보인 감소 폭은 그보다 훨씬 컸다. 손실이 컸다는 사실보다 더 불편했던 건, 내가 보고 있는 숫자와 실제 계좌 숫자가 서로 다른 언어를 쓰고 있었다는 점이었다.
이런 종류의 문제는 보통 전략 로직보다 더 위험하다. 진입이 한 번 잘못된 것도 문제지만, 그 잘못을 운영자가 제때 정확히 보지 못한다는 건 전혀 다른 층위의 리스크이기 때문이다. 이번 기록은 그래서 단순한 손실 회고가 아니라, post-only fallback 과정에서 실제 체결량을 어떻게 놓쳤고, 왜 내부 포지션과 거래소 포지션이 어긋나게 됐는지를 정리한 빌드로그다.
처음 이상했던 건 손실 크기보다 숫자의 불일치였다
처음엔 그냥 “이번 손실이 생각보다 컸네” 정도로 보였다. 그런데 로그를 다시 읽어보니 숫자가 맞지 않았다. 내부 기록상 포지션은 절반 정도만 열린 것처럼 읽혔고, 청산 손실도 비교적 얕아 보였다. 그런데 거래소에서 실제로 잡힌 노출은 거의 두 배였고, 계좌 감소 폭은 로그가 말해주는 수준보다 훨씬 깊었다.
이 순간 질문이 완전히 달라졌다. 이번 문제는 “왜 손절이 났는가”가 아니었다. 왜 시스템은 절반만 열린 줄 알았는데, 거래소는 두 배가 열린 상태였는가가 핵심이었다.
Reading The Mismatch
Internal View
보통 한 번의
손절처럼 읽혔다
로그와 텔레그램은 “의도한 size로 진입했고, 그 size만큼 손실이 났다”는 단순한 그림을 보여줬다.
Exchange View
실제 계좌는
더 큰 노출을 맞고 있었다
거래소 쪽에선 포지션이 더 크게 잡혀 있었고, 계좌 감소 폭도 내부 기록이 암시한 수준보다 훨씬 깊게 남았다.
Mismatch
리스크 계기판이
현실보다 작게 보였다
Cause
전략보다 먼저
체결량 추적이 무너졌다
Implication
내부와 거래소가
서로 다른 현실을 봤다
원인은 post-only가 실패한 게 아니라, 실패 과정에서 이미 열린 체결을 놓친 것이었다
문제는 post-only 진입 자체가 아니었다. 오히려 더 정확히 말하면, post-only가 실패하는 과정에서 일부 또는 전부 체결된 수량을 봇이 제대로 인식하지 못한 상태로 시장가 fallback을 다시 넣은 것에 가까웠다. 외형상으론 “진입 실패 후 fallback”처럼 보이지만, 실제로는 이미 문이 반쯤 열린 상태에서 다시 같은 문을 한 번 더 민 셈이었다.
이 지점이 특히 까다롭다. 거래소 응답은 비동기적이고, 주문 상태 조회가 항상 우리가 기대하는 순서로 돌아오지 않는다. 체감상으론 단순히 “주문이 거절됐다”처럼 보이는데, 실제론 그 직전 순간에 일부 체결이 발생했을 수 있다. 그리고 그걸 모르고 전체 잔량을 다시 시장가로 보내면, 내부 기록은 하나의 진입으로 남지만 거래소 포지션은 더 커져 버린다.
What Actually Happened
01
post-only 주문이 기대대로 유지되지 않았고, 조회와 취소 과정에서 이미 체결된 수량이 매끄럽게 반영되지 않았다.
02
엔진은 여전히 “아직 intended size가 충분히 들어가지 않았다”고 믿고, 시장가 fallback을 전체 기준으로 다시 시도했다.
03
거래소 실제 포지션은 더 커졌지만, 봇 내부 포지션은 요청한 size 기준으로 열려 손실도 더 작게 기록됐다.
그래서 이 버그는 손실보다 더 위험했다
이런 버그는 단순히 한 번 큰 손실이 났다는 점에서 끝나지 않는다. 더 위험한 건, 운영자가 보고 있는 리스크 계기판 자체가 이미 늦거나 작게 표시될 수 있다는 점이다. safe mode 판단, trailing 계산, 세션 해석, 사후 리포트까지 모두 “내부가 믿는 포지션 크기”를 기준으로 돌아가기 때문이다.
즉 이번 이슈는 거래 한 번이 꼬인 사건이 아니라, 실거래 체결과 내부 상태의 연결 고리가 느슨해져 있었던 사건에 더 가까웠다. 여기서부터는 전략 개선 문제가 아니라, 라이브 실행 계층의 신뢰를 다시 만드는 작업이 된다.
이번 사고의 진짜 무게는 손실 숫자 하나가 아니었다.
내부 포지션과 거래소 포지션이 서로 다른 현실을 보고 있었다는 점이 더 위험했다.
패치는 의외로 단순한 원칙에서 시작됐다
핵심 원칙은 하나였다. 이제부터 진입 성공 여부는 요청한 size가 아니라 거래소에서 실제로 확인된 filled size를 기준으로 믿는다. 다시 말해, fallback을 넣기 전에 먼저 현재 거래소 포지션 크기를 다시 읽고, 이미 충분히 체결됐다면 시장가를 더 넣지 않는다. 일부만 체결됐다면 남은 잔량만 채운다.
이 작업과 함께 엔진 쪽도 바꿨다. 진입 직후 내부 포지션을 여는 기준을 “내가 요청한 수량”이 아니라 “거래소에서 실제로 들어간 수량”으로 맞췄다. 만약 실제 체결량이 요청보다 더 크다면, 그건 정상 진입이 아니라 운영 사고에 가깝다. 그래서 그 경우엔 바로 safe mode로 들어가게 했다.
The Fix In One Principle
Before fallback
거래소 현재 포지션 size를 다시 읽고, 이미 체결된 수량이 있는지 먼저 확인한다.
Engine state
내부 포지션은 requested size가 아니라 actual filled size 기준으로 연다.
Exception handling
filled가 intended보다 크면 정상 진입이 아니라 운영 사고로 보고 safe mode로 즉시 막는다.
Takeaway
라이브 엔진은
요청한 수량이 아니라
실제로 체결된 수량을 믿어야 한다
라이브에서 진실은 늘 거래소 체결 쪽에 있다. 이번 패치는 엔진이 그 사실을 더 늦지 않게 받아들이도록 만든 수정이었다.
이 수정이 남긴 변화는 ‘손실 방지’보다 ‘해석 복구’에 가까웠다
물론 이런 패치는 앞으로 같은 종류의 과도한 진입을 직접적으로 줄여줄 가능성이 크다. 하지만 더 중요했던 변화는 따로 있었다. 이제는 로그, 텔레그램, 내부 포지션, 거래소 포지션이 같은 방향을 보도록 맞추는 기반이 생겼다는 점이다. 운영에서 해석이 어긋나기 시작하면 개선 방향도 흐려지는데, 이번 수정은 그 흐름을 다시 맞춰주는 작업이었다.
자동매매를 만들다 보면 전략의 엣지만큼이나 중요한 게 계기판의 정직함이라는 걸 자주 느낀다. 이번 패치도 결국 그 이야기였다. 손실을 완전히 없애는 건 어렵지만, 적어도 손실이 왜 그 크기로 났는지는 정확히 읽을 수 있어야 한다.
다음 편에서 이어질 것
흥미로운 건, 이번 사고가 여기서 끝나지 않았다는 점이다. 포지션 사이즈 불일치를 정리하고 나서 보니, 그 뒤엔 또 다른 질문이 남아 있었다. 보호주문이 거래소에서 사라졌을 때, 우리는 그걸 얼마나 빨리 알 수 있었나? 실제로 다음 단계에선 SL 주문 소실과 trailing 복구 타이밍이 더 큰 운영 리스크로 보이기 시작했다.
다음 기록은 그래서 자연스럽게 그쪽으로 이어질 것 같다. post-only fallback과 체결량 정합성을 먼저 맞춘 뒤, 이제는 보호주문이 비는 시간을 어떻게 줄였는지, 그리고 왜 30분이 아니라 1분 단위 감시가 필요했는지를 이어서 정리해볼 생각이다.
마무리
이번 빌드로그는 성과표를 예쁘게 만든 기록이 아니었다. 오히려 운영자가 믿고 있던 숫자가 어디서부터 어긋났는지를 추적한 기록에 더 가까웠다. 하지만 실전 자동매매에서 이런 종류의 작업은 생각보다 훨씬 중요하다. 전략이 좋더라도, 체결량과 포지션 상태를 잘못 읽으면 그 전략을 제대로 운영할 수 없기 때문이다.
그래서 이번 기록의 결론은 꽤 단순하다. 라이브 엔진은 요청을 믿는 시스템이 아니라, 실제 체결을 믿는 시스템이어야 한다. post-only fallback 사고를 정리하면서, 그 당연한 원칙을 다시 코드에 새긴 셈이다.
KEEP READING ON GEONULAB
실전 기록과 개념 정리를 한 흐름으로 이어서 읽어보세요
빌드로그는 실제 작업 기록에, 퀀트지식은 개념 정리에 집중합니다. 처음 방문했다면 Start Here에서 읽는 순서를 먼저 잡는 걸 추천합니다.
최신 글 흐름은 피드에서도 확인할 수 있습니다.
Email Updates
빌드로그와 퀀트지식 새 글이 올라오면 메일로 보내드립니다.
추후 봇 트레이딩 입문 PDF 소식도 가장 먼저 안내드릴게요.