퀀트 / 트레이딩8 분 읽기

고쳤던 버그가 8일을 더 살았습니다 — 게이트가 0만 잡고 있었거든요

페이퍼 봇이 매일 주문을 냈는데 8일간 한 주도 사지 못했습니다. 지정가가 다섯 세션 연속 73.23으로 굳어 있었고, 5일 전에 넣은 신선도 게이트는 한 번도 발화하지 않았습니다. 시세가 0으로 오지 않고 '옛 값'으로 왔기 때문입니다.

#quant-trading#reality-check#first-principles#automation-pipeline
왼쪽은 신선도 게이트가 검사하는 조건(가격 0 또는 나이 초과)만 걸러내고 나머지는 신뢰하는 상태, 오른쪽은 다섯 세션 연속 73.23이라는 정상 모양의 옛 값이 들어와 주문이 전부 미체결로 취소된 상태
왼쪽은 제가 막은 것, 오른쪽은 실제로 온 것입니다.

무인 페이퍼 봇 두 대가 매일 04:50에 지정가 주문을 냈습니다. launchd는 exit 0, 로그엔 에러가 없고, 감시판은 초록불이었습니다.

원장을 열어보니 8일간 한 주도 사지 못했습니다.

당신의 봇이 "정상"이라고 말할 때, 그게 뭘 만들었는지 세어보셨습니까? 저는 돌았다는 것만 확인하고 샀는지는 안 봤습니다.

다섯 세션, 똑같은 가격

08-07  73.83  FILLED
08-08  73.23  CANCELED
08-11  73.23  CANCELED
08-13  73.23  CANCELED
08-14  73.23  CANCELED
08-15  73.23  CANCELED     ← 실제 종가 76.79

같은 엔진을 쓰는 다른 종목도 32.04에서 똑같이 굳었습니다. 3배 레버리지 ETF 두 종목이 센트까지 같은 값으로 다섯 세션을 났습니다. 그런 시장은 없습니다.

지정가가 시장가보다 낮으니 매수 LOC는 체결될 리가 없습니다. 매일 주문을 내고, 매일 소멸하고, 이월금만 $550 쌓였습니다.

여기서 아픈 부분

이건 처음 보는 버그가 아니었습니다. 닷새 전에 같은 증상을 고쳤습니다.

그때 넣은 방어는 이랬습니다. 시세 응답의 호가 시각을 읽어서, 30분보다 오래된 값이면 0을 반환하고 그 세션을 통째로 건너뛴다. 코드에 주석까지 남겼습니다. "확인 못 하면 안 낸다."

그리고 8일 뒤 로그를 grep했습니다.

$ grep -c "현재가 조회 실패" logs/paperbot.tqqq.err.log
0

한 번도 발화하지 않았습니다.

여기서 갈립니다 — 당신이라면?

게이트를 넣었는데 안 잡혔습니다. 두 갈래가 보입니다.

A. 게이트가 배포 안 됐다고 의심한다. 프로세스가 옛 코드로 떠 있는 것 아닐까. B. 게이트는 도는데 조건에 안 걸린다고 의심한다.

저는 A부터 봤습니다. 프로세스 시작이 8월 12일 10:07, 파일 수정이 같은 날 09:32. 새 코드가 맞습니다.

그래서 B였습니다. 그리고 B가 훨씬 불편한 답입니다. 게이트는 시세가 0으로 올 때만 잡습니다. 그런데 실제로 온 건 0이 아니라 정상 응답 모양의 옛 값이었습니다. 필드도 다 있고 형식도 맞고 숫자도 그럴듯합니다. 게이트 입장에서는 트집 잡을 데가 없습니다.

실패는 예외로 오지 않았습니다. 정답인 척하고 왔습니다.

값이 아니라 변화를 봐야 했습니다

제가 검사한 건 "이 값이 유효한가"였습니다. 검사했어야 하는 건 "이 값이 살아있는가"입니다.

두 개를 넣었습니다.

하나는 계측입니다. 시세를 조회할 때마다 가격·호가시각·나이를 로그에 남깁니다. 실패 경로도 전부 다릅니다. 이전에는 나이 판정을 하고도 그 근거를 안 남겨서, 사후에 "게이트가 안 잡은 건지 잡을 게 없었던 건지"를 가를 수가 없었습니다. 8일치 로그에 단서가 한 줄도 없었던 이유입니다.

다른 하나는 판정입니다. 직전 세션과 센트까지 같은 값이면 제출을 건너뜁니다.

prev = self.ledger.last_buy_limit(date.isoformat())
if prev is not None and abs(ref - prev) < 0.005:
    log.error("시세 굳음 의심: 직전 세션과 동일한 %.2f → 제출 스킵", ref)
    return None

값이 반복되는 것도 실패로 셉니다. 레버리지 ETF가 두 세션 연속 동일가일 확률은 사실상 0이니까요. 1센트만 움직여도 정상으로 통과시키는 테스트를 같이 붙였습니다 — 임계를 너무 넓게 잡아 진짜 시세를 막으면 그게 더 나쁜 버그입니다.

자가진단 3개

  1. 당신의 검증은 "값이 없음"만 잡습니까, "값이 안 변함"도 잡습니까? 굳은 캐시·죽은 피드·옛 스냅샷은 전부 정상 응답 모양으로 옵니다.
  2. 판정 근거를 로그에 남깁니까? "실패했다"만 남기고 "무엇을 보고 통과시켰는지"를 안 남기면, 안 잡힌 실패는 흔적이 없습니다.
  3. 산출이 0인 연속 구간을 감시합니까? 실행 횟수는 매일 1이었고 체결은 8일간 0이었습니다. 앞의 숫자만 보면 완벽합니다.

솔직한 부분

이건 페이퍼 봇이라 잃은 돈이 없습니다. 잃은 건 8일치 관측입니다. 실계좌 전환 전에 사이클을 돌려보려던 기간이었는데, 그 기간의 원장에는 "매일 주문했고 아무것도 안 샀다"만 남았습니다.

그리고 근본 원인은 아직 모릅니다. 일요일에 발견해서 장이 닫혀 있었고, 시세 소스를 새 프로세스로 직접 찌르면 정상값(76.79)이 나옵니다. 오래 떠 있는 프로세스만 옛 값을 받는 셈인데, 왜 그런지는 관측을 못 했습니다. 그래서 추측으로 고치지 않고 다음 세션이 원인을 가르도록 계측만 심었습니다. 월요일 로그를 보고 이어서 쓰겠습니다.

한 가지는 확실합니다. 실패를 빈 데이터로 치환하지 말라는 것까지는 알고 있었는데, 이번엔 실패가 빈 데이터로도 오지 않았습니다. 멀쩡한 데이터로 왔습니다. 그건 값 자체를 봐서는 절대 못 가릅니다. 시간축에 놓고 움직이는지를 봐야 합니다.

지금 당신 파이프라인이 어제 쓴 외부 값과 오늘 쓴 값을 나란히 찍어보세요. 같으면, 그게 오늘 시장이 조용했다는 뜻인지 피드가 죽었다는 뜻인지 구분할 수단이 코드에 있습니까?

관련 글