10. 코드는 완성됐다. 시장은 아직 대답하지 않았다.

코드는 완성됐다. 시장은 아직 대답하지 않았다.

실전은 다른 세계다

백테스트에서 수익률 +6,214%를 찍었을 때, 나는 거의 다 끝났다고 생각했다. 모델 완성, 리스크 관리 완성, Walk-Forward 검증 통과, Dry-Run까지 마쳤다. 이제 실전에 넣기만 하면 되는 거 아닌가?

아니었다. 실전 운영은 완전히 다른 차원의 문제였다.

백테스트에서는 모든 데이터가 깨끗하고, API가 항상 응답하고, 네트워크가 끊기지 않는다. 하지만 실전에서는 새벽 3시에 바이낸스 서버가 응답 안 하고, 주문이 체결되는 데 2초가 걸리고, 데이터베이스 연결이 끊어진다.

만드는 것보다 운영하는 것이 100배 어렵다.

소프트웨어 공학에서 흔히 말하는 "개발은 전체 비용의 20%, 유지보수가 80%"라는 법칙을 몸으로 체감했다.


Binance API와의 전쟁

타임스탬프 동기화 문제

처음 실전 가동했을 때 가장 먼저 맞닥뜨린 에러가 이것이다:

BinanceAPIException: Timestamp for this request was 1000ms ahead of the server's time.

바이낸스 API는 요청의 타임스탬프가 서버 시간과 1초 이상 차이나면 거부한다. 내 서버 시간이 바이낸스 서버보다 1초 빠르면 모든 주문이 실패한다.

이 문제는 분산 시스템에서의 클럭 동기화(Clock Synchronization) 문제의 축소판이다. Leslie Lamport의 논문 "Time, Clocks, and the Ordering of Events in a Distributed System" (1978)이 이 분야의 고전인데, 핵심 통찰은 분산 시스템에서 "정확한 시간"이란 존재하지 않고, 이벤트 간의 인과적 순서(causal ordering)만 보장할 수 있다는 것이다. 결국 매 API 호출 전에 서버 시간을 확인하고 오프셋을 적용하는 방식으로 해결했다.

이런 문제는 백테스트에서는 절대 발견할 수 없다.

주문 체결 지연과 슬리피지

백테스트에서는 "이 가격에 진입"이라고 하면 그 가격에 정확히 진입된다. 하지만 실전에서 MARKET 주문을 넣으면 네트워크 지연, 서버 처리, 오더북 매칭, 체결 확인까지 가격이 움직인다. 이것이 슬리피지(Slippage)다.

$$S = \frac{P_{executed} - P_{expected}}{P_{expected}}$$

슬리피지는 체계적 비용이다. 변동성 높은 시점에서 0.05~0.1% 불리하게 체결되는 게 일상이다. 수수료 0.04%에 슬리피지 0.05%면, 매 거래마다 0.09%의 보이지 않는 비용이 발생한다. 740건의 거래에 누적되면 무시할 수 없는 숫자다.


대시보드: 숫자를 눈으로 보다

콘솔 로그만으로 시스템을 모니터링하는 건 한계가 있다. 특히 여러 날에 걸친 성과를 보거나, 패턴을 파악하려면 시각화가 필수였다. 그래서 별도의 대시보드 프로젝트를 만들었다.

처음에는 단순했다

처음 구현한 대시보드는 기본적인 것만 있었다. 총 거래 수, 승률, 일별 PnL 차트, 거래 기록 목록. 최소한의 기능이었지만, 콘솔 로그를 뒤지는 것보다 100배 나았다.

하지만 실전에서 돌려보니 필요한 것들이 하나둘 생겨났다. 대시보드는 꾸준히 진화했다.

PnL 계산 오류와의 사투

대시보드를 처음 만들고 가장 먼저 발견한 건 PnL 계산 오류였다. 레버리지와 수수료를 반영하는 방식에 미세한 차이가 있어서, 대시보드에 표시된 수익과 실제 바이낸스 계좌의 수익이 달랐다. 차이는 0.1% 미만이었지만, 이게 수백 건의 거래에 걸쳐 누적되면 꽤 큰 오차가 된다.

부동소수점 연산의 고전적인 문제이기도 했다. 소수점 이하 자릿수 처리가 바이낸스의 정밀도와 내 계산의 정밀도 사이에서 어긋나고 있었다. 이건 이전에 거래 동기화 스크립트에서 0.001 허용 오차를 둔 것과 같은 맥락이다.

모바일에서도 봐야 한다

출근길 지하철에서, 점심시간에, 자기 전에, 포지션이 열려 있으면 수시로 확인하고 싶어진다. PC에서만 보는 대시보드로는 부족했다. 모바일 반응형을 추가하고, 메뉴를 햄버거 버튼으로 바꿨다.

그 다음으로 추가한 건 일별 수익 캘린더였다. 월별로 달력 형태로 각 날의 수익/손실을 색깔로 보여주는 뷰. 초록색이 많은 달은 기분이 좋고, 빨간색이 모이기 시작하면 긴장한다. 이 캘린더가 정서적으로 가장 큰 영향을 미친 기능이었다. 데이터를 "날짜"라는 직관적 프레임에 넣으니, 추상적인 누적 수익보다 체감이 컸다.

청산 사유 분석

거래 기록에서 가장 궁금한 건 "왜 이 포지션이 닫혔는가"다. TP(익절)로 닫혔는지, SL(손절)로 닫혔는지, 트레일링 스탑으로 닫혔는지, 시간 초과로 닫혔는지. 이걸 시각화하자 패턴이 보이기 시작했다.

특정 시간대에 SL이 집중되거나, 특정 요일에 트레일링 스탑 비율이 높거나. 이런 패턴은 숫자만 봐서는 알 수 없고, 차트로 봐야 드러난다. 대시보드 고도화에서 trade_mode 필터와 청산 사유 분석 기능을 추가한 건 이 때문이었다.

모델 신호 반전 실험

가장 흥미로웠던 기능은 MODEL_REVERSAL 토글이었다. 모델의 예측 신호를 정반대로 뒤집어 실행하는 옵션이다. LONG 시그널이 나오면 SHORT으로, SHORT이면 LONG으로.

왜 이런 기능을 만들었을까? "모델이 틀리는 패턴이 일관적이라면, 반대로 하면 수익이 나지 않을까?"라는 가설이었다. 물론 단순한 반전이 수익을 보장하지는 않는다. NO_TRADE 판단이 있기 때문이다. 하지만 특정 시장 레짐에서 모델의 방향성 예측이 체계적으로 틀리는 구간이 있다면, 반전이 유효할 수 있다.

대시보드에 이 토글을 넣고, 반전 모드의 성과를 정상 모드와 나란히 비교할 수 있게 만들었다. 결론부터 말하면, 반전이 의미 있는 구간은 생각보다 적었다. 하지만 이 실험 자체가 "모델의 실패 패턴을 분석한다"는 관점의 전환을 가져다줬다.

모델별 수익 분석

마지막으로 추가한 기능은 모델별 수익 분석 페이지였다. 누적/일별 PnL 차트를 모델 버전별로 비교하고, 성과 테이블로 한눈에 볼 수 있게 만들었다.

v2.5와 v2.7을 같은 기간에 대해 나란히 놓으니, 숫자가 아닌 곡선의 형태가 말해주는 게 있었다. v2.5의 자본 곡선은 지그재그로 출렁이며 서서히 하락하고, v2.7은 부드럽게 우상향한다. 같은 시장, 같은 기간인데 곡선의 성격이 완전히 다르다. 모델의 "질적 차이"를 가장 직관적으로 보여주는 화면이었다.


데이터 정합성과의 싸움

sync_trades: 유령 포지션 사냥

자동매매 시스템이 돌아가면 거래 기록이 쌓인다. 문제는 바이낸스의 거래 기록과 내 데이터베이스의 기록이 어긋날 수 있다는 것이다.

가장 흔한 시나리오: TP 또는 SL 주문이 바이낸스에서 자동 체결되었는데, 내 시스템이 그 순간 API를 호출하지 않으면 DB에는 아직 "OPEN" 상태로 남아 있다. 분산 시스템에서의 최종 일관성(Eventual Consistency) 문제다.

이걸 해결하기 위해 거래 동기화 스크립트를 만들었다. DB에서 "OPEN" 상태인 거래를 조회하고, 바이낸스 API에서 최근 24시간 거래 히스토리를 가져와서, 수량을 매칭해 실제로는 청산된 거래를 DB에 업데이트한다. 수량 매칭에서 0.001의 허용 오차를 두었다. 부동소수점 정밀도 차이 때문에 이 허용 오차가 없으면 매칭이 안 돼서 "유령 포지션"이 DB에 영원히 남는다.

가장 먼저 추가한 기능은 dry-run 모드였다. 실제 DB를 건드리기 전에 "이 거래가 매칭될 것인가"를 먼저 확인할 수 있다. 운영 환경에서 DB를 잘못 건드리면 복구가 어렵기 때문이다.

토큰 만료와 강제 리다이렉트

대시보드 인증 토큰이 만료되었을 때, 조용히 빈 화면만 보여주던 초기 버전은 꽤 당황스러웠다. "왜 데이터가 안 나오지?" 하고 한참을 삽질한 후에야 토큰 문제라는 걸 알았다. 만료 시 로그인 페이지로 강제 리다이렉트하는 처리를 추가했다. 사소하지만, 이런 것들이 "쓸 수 있는 도구"와 "만들어만 놓은 도구"의 차이를 만든다.


운영의 일상

자동 재시작과 가용성

프로그램은 죽는다. 메모리 릭, 예상 못한 예외, 네트워크 타임아웃. 중요한 건 죽었을 때 자동으로 살아나는 것이다.

시스템 가용성(Availability)은 다음과 같이 정의된다.

$$A = \frac{MTBF}{MTBF + MTTR}$$

여기서 $MTBF$(Mean Time Between Failures)는 장애 간 평균 시간, $MTTR$(Mean Time To Repair)은 장애 복구 평균 시간이다. 자동 재시작이 없으면 $MTTR$이 인간의 반응 시간(새벽이면 수 시간)에 의존한다. 자동 재시작을 추가하면 $MTTR$이 수십 초로 줄어들어 $A$가 크게 향상된다.

24시간 편의점에 비유하면, MTBF는 직원이 쓰러지기까지 버티는 시간이고 MTTR은 대타가 도착하는 시간이다. 사장이 직접 달려오면 2시간 걸리지만, 바로 옆집에 대기 알바가 있으면 30초면 교대한다.

주기적 헬스체크로 프로세스 생존을 확인하고, 죽어있으면 재시작하는 체계를 만들었다. 간단하지만 이게 없으면 새벽에 시스템이 죽었을 때 다음 날 아침에야 알게 된다.

텔레그램 알림

중요한 이벤트는 텔레그램으로 알림을 보낸다. 포지션 진입/청산, 서킷브레이커 발동, 시스템 에러, 일일 수익 요약.

새벽에 잠자면서도 폰에서 알림을 확인할 수 있다. "BTCUSDT LONG +12.5% 익절" 같은 알림이 오면 기분 좋게 다시 잔다. "서킷브레이커 발동: 3연패" 같은 알림이 오면... 눈이 번뜩 떠지지만, 시스템이 스스로 멈춘 거니까 뒤척이다 다시 잔다.


백테스트와 실전의 갭

이 시리즈를 쓰면서 계속 강조한 것이 "백테스트 결과를 의심하라"는 것이다. 실전과 백테스트의 차이를 만드는 주요 요인을 정리하면:

요인 백테스트 실전 영향
슬리피지 $S = 0$ $S \approx 0.05\%$ 거래당 수익 감소
수수료 고정 0.04% 0.04% + 변동 동일~소폭 증가
API 실패 0% ~0.1% 진입/청산 지연
유동성 무한 유한 대량 주문 시 불리한 체결
데이터 품질 완벽 가끔 누락/지연 잘못된 시그널 가능

이 요인들을 종합하면, 백테스트 수익률의 약 60~80%가 실전에서 실현 가능하다고 보는 것이 현실적이다. 하지만 이런 "할인율"을 항상 염두에 두어야 한다.


그래서, 수익률은?

4개월간의 여정이었다. v1.0의 55.58%에서 시작해서, 로그수익률을 배웠고, Transformer의 32,687%라는 환상에 빠졌다가, SMOTE의 독을 경험하고, 승률 68%의 함정에 걸렸고, Triple-Barrier로 질문 자체를 바꾸고, 67개 피처를 설계하고, 파산하지 않는 리스크 관리를 만들고, Walk-Forward와 Dry-Run으로 검증하고, 대시보드를 만들어 숫자를 눈으로 봤다.

코드는 완성됐다. 모델은 학습됐다. 시스템은 돌아가고 있다.

그래서 그 다음에 수익률이 어떻게 되었냐고?

그건 아무도 모른다.

백테스트의 +6,214%는 과거 데이터에 대한 시뮬레이션이다. Walk-Forward로 일관성을 확인했고, Dry-Run으로 실전 마찰을 검증했다. 하지만 내일의 시장은 어제의 시장과 다르다. 비트코인이 내일 어떻게 움직일지, 연준이 뭘 할지, 시장의 미시구조가 어떻게 변할지, 이건 모델이 아니라 세상의 영역이다.

내가 확신할 수 있는 건 딱 하나다. 이 시스템이 내가 만들 수 있는 최선이라는 것. 수학적으로 가능한 검증은 다 했다. 코드에서 할 수 있는 건 다 했다. 나머지는 시장이 결정한다.

어쩌면 이 시리즈 전체가 하나의 교훈으로 수렴하는 것 같다.

올바른 질문을 하는 것이, 올바른 답을 찾는 것보다 중요하다.

v1.0에서 "가격이 오를까 내릴까?"라고 물었을 때, 답은 60%를 넘지 못했다. "어떤 배리어에 먼저 닿을까?"로 질문을 바꾸자, 같은 모델이 88%를 찍었다. "좋은 모델을 만들자"가 아니라 "좋은 질문을 하자"로 관점을 바꾼 순간, 벽이 사라졌다.

이건 트레이딩에만 해당되는 이야기가 아닌 것 같다. 개발에서도, 인생에서도, 답이 안 나올 때 필요한 건 더 정교한 해법이 아니라 더 나은 질문인 경우가 많다.

시스템은 계속 돌아가고 있다. 텔레그램 알림은 계속 온다. 대시보드의 자본 곡선은 매일 한 점씩 찍히고 있다.

그 곡선이 어디를 향할지는, 정말로, 아무도 모른다.