(REST + WebSocket, 64bit)
"종목 여러 개에 격자를 깔아 두고, 산 차수만 따로 익절하고, 팔리면 그 자리에 다시 사고 싶다." 2026년 8월에 납품한 그리드 봇입니다. 처음 명세서는 구 OpenAPI+(OCX)였는데 신 REST API로 바꿔 만들었고, 납품 뒤 실계좌 운용에서 나온 문제를 고치느라 v1.0.0에서 v1.8.1까지 열세 번 버전이 올라갔습니다. 그 과정까지 그대로 적습니다.
의뢰인은 이미 손으로 그리드 매매를 하고 계셨습니다. 기준가에서 몇 % 떨어질 때마다 한 차수씩 사고, 각 차수가 몇 % 오르면 그 차수만 팔고, 팔린 자리는 다시 사 두는 방식입니다. 종목이 서너 개를 넘어가면서 미체결 주문을 사람이 관리하기가 어려워졌고, 15:30 이후 넥스트레이드(NXT) 시간외에도 이어서 돌리고 싶다는 게 출발점이었습니다.
명세서에는 키움 OpenAPI+(OCX)라고 적혀 있었습니다. 저희가 REST로 바꾸자고 제안한 이유는 두 가지입니다. OCX는 영웅문을 백그라운드에 항상 켜 둬야 하고 32비트 파이썬을 써야 해서 설치와 운영이 번거롭습니다. REST는 그 둘이 다 필요 없습니다. 대신 앱키 발급과 지정단말기 등록이 필요한데, 이건 상담 때 화면을 같이 보면서 끝냈습니다.
차수 가격은 기준가 × (1 + 증감률)^(기준회차 − 차수번호)로 계산해 호가단위에 맞춥니다.
기준가 100,000원, 증감률 3%, 기준회차 5차라고 하면 대략 이런 표가 만들어집니다.
| 차수 | 매수가 | 익절가 (+2.7%) | 동작 |
|---|---|---|---|
| 3차 | 106,090 | 108,950 | 기준가 위 — 대기 |
| 4차 | 103,000 | 105,780 | 대기 |
| 5차 (기준) | 100,000 | 102,700 | 지정가 매수 걸림 |
| 6차 | 97,090 | 99,710 | 지정가 매수 걸림 |
| 7차 | 94,260 | 96,800 | 지정가 매수 걸림 |
어느 차수든 체결되면 그 차수만 +이득률에 익절 지정가를 걸고, 팔리면 다시 같은 자리에 매수를 겁니다. 이게 방식 A입니다. 여기에 전체청산가격을 따로 두어서, 가격이 거기 닿으면 보유 전 차수를 한 번에 정리합니다(방식 B). 두 방식은 동시에 켜 둡니다.
trde_tp=0). 전체청산만 시장가를 고를 수 있습니다.이 프로그램은 테스트가 902개(파일 6개) 돌아가는 상태로 나갔는데도, 실계좌에서 문제가 여럿 나왔습니다. 대부분 "동작은 하는데 조용히 틀린" 유형이라 증상만 보고는 원인을 못 찾았고, 읽기 전용 진단 스크립트로 실제 응답을 찍어 보며 잡았습니다. 셋만 골라 적습니다.
15:30 이후 어떤 종목의 익절 매도가 전부 거부됐습니다. 표 메모에는 return_code=20 … 넥스트레이드 오류만 남았고요.
원인은 단순했습니다. NXT는 주식만 취급하고 ETF·ETN은 대상이 아닌데, 15:30 이후를 무조건 NXT로 라우팅하고 있었습니다.
더 나쁜 건, 거부됐을 때 정규장으로 돌리는 폴백이 전체청산 경로에만 있고 익절 매도 경로에는 없었다는 점입니다. 거부 → 대기 → 또 NXT, 무한 반복.
종목별로 nxt_blocked를 기억해 한 번 거부된 종목은 정규장 전용으로 돌리고,
매수·매도·청산이 같은 판정 함수 하나를 쓰도록 통일했습니다.
의뢰인이 4만 주를 들고 있는 종목에서 [중지] → [그리드 생성/적용] → [시작]을 눌렀더니 10차 한 곳에 15,302주가 몰리고 나머지 차수는 전부 다시 매수가 나갔습니다.
재생성 함수가 차수 객체를 새로 만들면서 보유수량을 0으로 지웠고, 시작 시 잔고 대조가 기존 물량을 기준회차 한 곳에 넣어 버린 겁니다.
경고문에도 "수동 편집값이 초기화됩니다"라고만 있었지 보유 얘기는 없었습니다.
지금은 재생성할 때 보유·주문·주문번호를 매입가가 가까운 새 차수로 이월하고, 익절가도 새 그리드 가격이 아니라 실제 매입가 기준으로 다시 계산합니다. 확인창에는 "보유 N주를 이렇게 처리합니다"가 수치로 뜹니다.
의뢰인이 화면 현재가가 영웅문과 조금 다르다고 지나가듯 말씀하셨습니다. 확인해 보니 시간외에는 주문이 NXT로 나가는데
현재가는 ka10001 기본 조회(KRX)를 쓰고 있었습니다. 그날 삼성전자가 KRX 231,000원 / NXT 235,500원, 1.9% 차이.
증감률 3% 그리드에서는 차수가 한 칸 가까이 밀리는 값입니다.
종목코드에 _NX를 붙이면 NXT 시세가 온다는 걸 확인하고, 시세가 들어오는 세 경로를 전부 "지금 주문이 나가는 거래소 기준"으로 고쳤습니다.
사용자가 중요하지 않다고 한 표시 불일치가 계산 불일치의 증상인 경우가 많다는 걸 다시 배웠습니다.
다종목은 종목마다 독립된 그리드 엔진을 두고, REST 클라이언트·주문 발송기·WebSocket·알림·매매내역은 공유하는 구조입니다. 시세·체결·잔고 이벤트가 종목코드로 걸러지니 이벤트를 종목별 엔진에 나눠 주기만 하면 서로 간섭이 없습니다. 계좌를 나눌 필요도 없습니다.
.env 템플릿아니요. REST라서 영웅문이 필요 없고 32비트 제약도 없습니다. 대신 앱키 발급과 지정단말기 등록이 선행돼야 하는데, 절차는 키움 REST 앱키 발급 가이드에 화면과 함께 적어 뒀습니다.
안 됩니다. 위에 적은 대로 NXT는 주식만 취급합니다. 프로그램이 한 번 거부된 종목을 기억해서 정규장 전용으로 돌립니다. NXT 자체에 대한 정리는 키움 REST API 자동매매 가이드를 보시면 됩니다.
종목마다 기준가·증감률·차수 수·차수당 수량·이득률·전체청산가격을 따로 둡니다. 저장하면 다음 사이클부터 적용되고 프로그램을 다시 켤 필요는 없습니다. 다만 보유 중에 그리드를 다시 만들면 물량이 새 차수로 이월된다는 점은 확인창에서 한 번 더 보게 됩니다.
주문 요청의 실제 형태(kt10000·trde_tp·dmst_stex_tp)는
키움 REST API 주식 주문 정리에 있습니다.