자본시장과 결제 · 스냅숏과 증분으로 호가창 다시 세우기 · 이론
스냅숏과 증분을 잘못 이으면 호가창은 조용히 틀린다
한 줄 요약
시세는 스냅숏 한 장과 그 뒤로 이어지는 증분 갱신으로 온다. 스냅숏이 찍힌 순번과 증분의 순번을 겹치게 이어야 하고, 순번이 한 번이라도 끊기면 증분을 더 대지 말고 새 스냅숏을 기다려야 한다. 이 규칙을 빠뜨린 호가창은 멈추거나 비지 않는다. 그냥 조금 틀린 채로 계속 돈다.
왜 이게 필요했나
호가창 전체를 매번 보내면 대역폭이 감당되지 않는다. 그래서 어느 시세 피드든 같은 모양을 쓴다. 지금 상태를 한 장 찍어 보내고(스냅숏), 그 뒤로는 바뀐 가격 단계만 하나씩 보낸다(증분). 받는 쪽은 스냅숏으로 호가창을 세운 다음 증분을 차례로 얹는다.
문제는 이음매다. 스냅숏은 찍는 데 시간이 걸리고, 그 사이에도 증분은 계속 나간다. 그래서 스냅숏에는 "여기까지는 이미 반영되어 있다" 는 순번이 함께 박혀 나간다. 받는 쪽은 스냅숏보다 앞선 순번의 증분을 버리고 그 뒤부터 얹어야 한다. 이 값을 무시하고 받은 증분을 전부 얹으면 이미 반영된 변경이 한 번 더 적용되고, 반대로 스냅숏이 오기 전 증분을 전부 버리면 그 사이의 변경이 사라진다. FIX 계열 규격이 재개 시점을 다루면서 "마지막으로 처리한 순번" 을 늘 함께 나르는 이유가 여기에 있다([FIX Trading Community 표준 문서](https://www.fixtrading.org/standards/), 필드 의미는 [FIXimate](https://fiximate.fixtrading.org/) 에서 찾을 수 있다).
어떻게 동작하나
증분의 동작은 셋뿐이다. 추가, 변경, 삭제. 그런데 대부분의 피드는 동작 이름을 보내지 않는다. 수량 0 이 삭제이고, 0 이 아닌 값은 그 가격이 호가창에 있으면 변경, 없으면 추가다. 즉 동작은 메시지가 아니라 받는 쪽이 들고 있는 상태가 정한다. 그래서 같은 증분 파일을 받아도 시작 상태가 다르면 결과가 달라진다. 수량 0 을 값으로 저장해 버리는 구현은 수량 0 짜리 가격 단계를 남기고, 그 단계가 최우선 호가 자리를 차지하면 화면에는 "0주 대기" 가 최우선으로 뜬다.
호가창에는 지켜야 할 불변식이 있다. 최우선 매수가는 최우선 매도가보다 낮아야 한다. 둘이 같거나 뒤집히면 교차한 것이고, 교차한 호가창은 현실에 없다. 그 상태가 화면에 떠 있다는 것은 내 재구성이 틀렸다는 뜻이다. 그리고 가격당 수량은 양수여야 하며, 정렬은 늘 유지되어야 한다.
불변식을 재는 계산에는 함정이 하나 더 있다. 스프레드가 한 호가 단위인지 보려고 두 가격을 빼는 순간, 이진 부동소수로는 답이 나오지 않는다. 55.15 에서 55.10 을 빼면 0.04999999999999716 이 되어 0.05 와 같지 않다. 가격은 십진 소수라서 십진으로 다뤄야 한다([decimal 모듈 문서](https://docs.python.org/3/library/decimal.html)). 이 실수는 예외를 내지 않기 때문에 "한 호가 스프레드가 한 번도 없었다" 는 조용한 0 으로 나타난다.
마지막이 재동기화다. 순번이 끊겼다는 것은 내가 못 받은 변경이 있다는 뜻이고, 그 변경이 무엇이었는지는 알 방법이 없다. 이때 증분을 계속 얹으면 호가창은 끊긴 자리에서 갈라져 영영 돌아오지 않는다. 옳은 처리는 증분 적용을 멈추고 다음 스냅숏을 기다리는 것이다. 그래서 피드는 스냅숏을 주기적으로 다시 뿌린다.
현장에서 만나는 모습
한 팀에서 "최우선 매수가가 매도가보다 높게 뜬다" 는 제보가 들어왔다. 화면을 새로 고치면 멀쩡해져서 화면 문제로 접수됐다. 실제로는 몇 분 전 짧은 갭이 있었고, 그때 사라졌어야 할 매수 단계가 그대로 남아 있었다. 새로 고침이 고친 것은 화면이 아니라 호가창이었다. 그 사이 몇 분 동안 나간 주문은 없는 호가를 보고 값을 정한 것이다.
또 하나 자주 보는 것이 지연 도착이다. 순번은 과거인데 나중에 도착한 줄을 도착 순서대로 얹으면, 이미 덮어쓴 값이 옛 값으로 되돌아간다. 이것도 오류 없이 지나간다. 도착 순서가 아니라 순번 순서로 얹고, 이미 지나간 순번은 버리는 것이 유일한 방어다.
세 번째는 조금 다른 종류의 사고다. 재구성한 호가창을 한 곳에서만 들고 있지 않을 때 생긴다. 화면과 전략 엔진이 각자 증분을 얹으면 두 호가창은 같은 입력을 받고도 서로 달라진다. 한쪽만 갭을 감지해 재동기화하고 다른 쪽은 계속 얹기 때문이다. 이런 자리에서는 "어느 쪽이 맞는가" 를 사람이 눈으로 가릴 수 없다. 그래서 재구성은 한 곳에서 하고 결과를 나눠 주거나, 최소한 두 호가창이 어느 순번까지 반영했는지를 함께 들고 다니게 만들어야 한다. 조사할 때 가장 먼저 묻는 것도 그것이다 — 이 화면은 몇 번 순번까지 본 것인가.
받는 쪽을 고칠 때 반드시 함께 정해야 하는 것이 하나 더 있다. 재동기화를 하는 동안 호가창을 무엇으로 보여 줄 것인가다. 옛 호가창을 그대로 두면 사람은 그것이 지금 시세라고 믿고, 비워 버리면 주문 화면이 멈춘 것처럼 보인다. 어느 쪽이든 그 상태라는 사실이 화면과 로그에 드러나야 한다. 이 선택을 미뤄 두면, 갭을 제대로 감지하도록 고친 뒤에 오히려 "화면이 자꾸 비었다" 는 신고가 늘어난다.
다음 실습에서 할 것
네 종목이 한 채널로 흐르는 합성 피드를 만들어, 스냅숏 한 장에서 호가창을 세우고 증분 1,300여 줄을 얹습니다. 순번이 겹치는 구간을 버리는 규칙을 넣고, 호가창이 교차하는 순간의 순번을 찾고, 끊긴 자리에서 계속 얹은 결과와 새 스냅숏에서 다시 세운 결과가 몇 개의 가격 단계에서 갈라지는지 셉니다. 마지막에는 두 규칙을 모두 넣은 구현으로 전체 스트림을 돌려 기준 스냅숏과 한 단계도 어긋나지 않는 것을 확인합니다.