퀀트 투자 자동매매 시스템 - 실전 계좌 적용편
몇 주 동안 개발일지에 조각조각 나왔던 실거래 준비 얘기를, 이번엔 구조로 한 번에 정리했습니다
지난 몇 주 동안 개발일지에 실거래 준비 얘기가 조각조각 나왔습니다. 코드 감사에서 버그를 잡았다, 증권사를 바꿨다, 첫 실주문을 냈다 같은 이야기들이었는데, 오늘은 그 조각들을 한 번에 모아서 구조로 설명해보려 합니다.
미리 말씀드릴 게 하나 있습니다. 실제로 어떤 종목을 살지 고르는 로직("AI 앙상블"이라고 불렀던 부분)은 여기서 다루지 않습니다. 이 글은 그 판단이 실제 주문으로 안전하게 이어지도록 감싸는 층, 그러니까 "무엇을 살까"가 아니라 "어떻게 안전하게 살까"에 관한 이야기입니다.
돈이 나가는 문은 하나여야 한다
실거래 준비를 시작하면서 제일 먼저 정한 원칙이 있습니다. 실제로 증권사에 주문을 보내는 코드 경로는 딱 하나여야 한다는 것이었습니다.
전에는 기능별로(리밸런싱, 신호 매매 등) 각자 나름의 주문 코드를 갖고 있었습니다. 편하긴 한데, 한쪽에서 안전장치를 고쳐도 다른 쪽은 그대로 남는 위험이 있었습니다.
그래서 모든 경로를 하나의 관문으로 합쳤습니다. 어떤 기능이 주문을 내고 싶어도 반드시 이 관문 하나를 거쳐야만 실제로 증권사에 도달합니다. 안전장치를 한 곳에 몰아넣을 수 있게 된 게 가장 큰 이득이었습니다.
증권사는 갈아 끼울 수 있어야 한다
이 관문은 특정 증권사에 딱 붙어있지 않습니다. 증권사마다 다른 세부사항(인증 방식, 요청 형식)은 작은 어댑터 하나에만 담아두고, 나머지 로직은 증권사와 무관하게 짜뒀습니다.
실제로 이 구조 덕을 봤습니다. 최근에 증권사를 하나 바꾸기로 했을 때 손댄 부분은 이 어댑터 하나뿐이었고, 나머지 안전장치나 로직은 전혀 안 건드렸습니다. 미리 나눠두지 않았다면 훨씬 큰 작업이 됐을 겁니다.
성공, 실패, 그리고 "알 수 없음"
주문을 넣고 결과를 받을 때, 흔히 성공/실패 둘로만 나누기 쉽습니다. 그런데 실제로 겪어보니 세 번째 경우가 있었습니다 — 응답 자체를 못 받아서 성공인지 실패인지 알 수 없는 경우입니다.
이걸 실패로 뭉뚱그리면 위험합니다. "실패했나 보다" 하고 수동으로 다시 주문을 넣었다가, 사실 원래 주문도 이미 체결돼서 결과적으로 두 번 사버릴 수 있기 때문입니다.
그래서 결과를 성공·실패·알 수 없음, 이렇게 세 가지로 명확히 나눴습니다. "알 수 없음"이 나오면 곧바로 재주문하는 대신, 실제로 어떻게 됐는지부터 확인하는 절차를 타게 만들었습니다.
같은 주문을 두 번 안 내는 법
이중 주문을 막는 데는 두 가지 장치를 같이 씁니다.
하나는 주문마다 고유한 이름표를 붙이는 겁니다. 같은 이름표로 또 주문이 들어오면, 이미 처리한 주문으로 인식하고 걸러냅니다.
다른 하나는 순서입니다. "이 주문을 내겠다"는 기록을 먼저 안전하게 남긴 다음에야 실제로 주문을 냅니다. 이 기록 자체가 실패하면 주문도 내지 않습니다 — 기록도 안 됐는데 주문만 나가면, 나중에 그 주문의 존재를 아무도 모르는 상태가 될 수 있기 때문입니다.
장부와 실제가 다르면, 실제를 믿는다
주문을 낸 뒤에는 실제로 어떻게 체결됐는지를 증권사 쪽 기록과 대조합니다. 처음엔 이 대조가 정규 거래시간 하나만 보고 있었는데, 그 시간 밖에서 체결된 주문은 아예 안 보이는 사각지대가 있다는 걸 나중에 알았습니다.
지금은 가능한 모든 체결 경로를 다 확인해서 대조합니다. 우리 쪽 장부가 뭐라고 되어 있든, 실제 체결 기록과 다르면 실제 쪽을 믿고 장부를 고칩니다.
내 몫과 계좌 전체는 다르다
이 봇은 계좌 전체가 아니라 계좌 안의 한 부분만 관리합니다. 그래서 "봇이 파악한 보유 수량"과 "계좌의 실제 보유 수량"이 항상 완전히 같아야 한다고 가정하면 위험합니다. 계좌에 봇이 모르는 다른 보유분이 섞여 있을 수 있기 때문입니다.
대신 더 약하지만 실제로 맞는 조건을 씁니다. "봇이 파악한 몫은 항상 실제 보유량보다 적거나 같아야 한다"는 조건입니다. 이 조건이 깨지면 뭔가 어긋났다는 뜻이라, 바로 멈추고 알림이 갑니다.
멈추는 것도 설계다
손실을 막는 장치는 두 층으로 나눠뒀습니다.
한 층은 자동입니다. 정해둔 기준을 넘는 손실이 나면 그 부분만 스스로 멈춥니다.
다른 한 층은 사람입니다. 언제든 전체를 즉시 얼릴 수 있는 마스터 스위치를 따로 뒀습니다. 자동 장치가 미처 못 잡는 상황을 위한 마지막 안전판입니다.
멈추는 것과 청산하는 것은 다른 개념으로 뒀습니다. 멈추면 그 상태 그대로 얼릴 뿐이고, 갖고 있던 걸 파는 것과는 다릅니다.
안전장치도 혼자 살아남아야 한다
이 안전장치들이 실제로 도는 프로세스는, 제가 대화하는 채팅 프로그램과는 별개로 뒀습니다. 예전엔 몇몇 백그라운드 작업이 채팅 봇 프로세스 안에서 같이 돌고 있었는데, 채팅 프로그램을 재시작하면 이 작업들도 같이 끊기는 문제가 있었습니다.
지금은 감시·정산·잔고기록 같은 작업을 전부 별도의 상시 서비스로 분리해뒀습니다. 승인 대기 중인 요청도 메모리가 아니라 디스크에 남겨서, 봇이 재시작돼도 사라지지 않게 했습니다.
마무리
하나하나 보면 특별할 것 없는 장치들입니다. 이중 주문 방지, 상태 확인, 장부 대조, 킬 스위치 — 어디서나 들어봤을 이름들입니다.
다만 혼자 만드는 프로젝트에서 이런 걸 다 갖추려면, 처음부터 완벽하게 설계하기보다는 실수를 하나씩 겪으면서 채워나가는 방식이 될 수밖에 없었습니다. 여기 적은 장치들 대부분이 실제로 사고나 코드 감사에서 발견된 구멍을 메우면서 하나씩 생겼습니다.
전체 구조가 궁금하시면 프로젝트 구조 글(새 창)을 먼저 보시는 게 좋습니다. 이 글은 그중 "트레이딩봇" 부분이 실제로 어떻게 짜여 있는지를 더 자세히 푼 버전입니다.