(OMS 3) 시세 수신
OMS에서 거래소 시세를 수신하는 방법을 다룹니다. 코스콤 MDCS 가입부터 접속표준서 확인, 파생 호가 메시지 구조까지 살펴봅니다.
이전 글: 중요한 숫자 관리는 정수로
이 글은 매매 시스템 시리즈의 글입니다.
다음 글: 오더북이 필요한 이유
시세 수신은 매매 시스템의 첫 단계입니다. 거래소에서 보내는 호가, 체결 정보를 받아야 전략이 시작됩니다. 한국거래소 데이터를 예시로 시세를 가공하는 과정을 살펴보겠습니다.
코스콤 MDCS (Market Data Center Services)
한국거래소 시세 전문 (메시지) 프로토콜은 다음 사이트에서 확인할 수 있습니다. 회원 가입을 하면 전문 스펙이 바뀔 때마다 메일도 보내 줍니다.
먼저 회원 가입을 하고, 로그인하면 접속표준서를 다운받을 수 있습니다
접속표준서 다운로드
메뉴에서 접속표준서 → 실시간시장정보로 이동해서 최신 파일을 다운받습니다. 현재 시점 (2026-01-17) 최신 파일은 다음과 같습니다.
1
접속표준서(정보분배-UDP_TCP실시간)_v2.010-배포용.xlsx
이 엑셀 파일에 시세 메시지의 포맷이 정의되어 있고, 메시지 종류별로 시트가 나뉘어 있습니다.
예시: 파생 호가 메시지 구조
예시로 파생상품(선물, 옵션)의 우선호가 5단계 메시지 구조를 한번 살펴볼까요? 인터페이스정의서에서 볼 수 있습니다:
| 필드명 | 설명 | 타입 | 길이 | 누적 |
|---|---|---|---|---|
| Data Category | 데이터 구분 | String | 2 | 2 |
| Information Category | 정보 구분 | String | 3 | 5 |
| Message sequence number | 메시지 일련번호 | Int | 8 | 13 |
| Board ID | 보드 ID | String | 2 | 15 |
| Session ID | 세션 ID | String | 2 | 17 |
| ISIN Code | 종목코드 | String | 12 | 29 |
| A designated number for an issue | 종목 지정번호 | Int | 6 | 35 |
| Processing Time of Trading System | 처리 시각 | String | 12 | 47 |
| Ask Level 1 price | 매도 1호가 | Double | 9 | 56 |
| Bid Level 1 price | 매수 1호가 | Double | 9 | 65 |
| Ask Level 1 volume | 매도 1잔량 | Int | 9 | 74 |
| Bid Level 1 volume | 매수 1잔량 | Int | 9 | 83 |
| Ask Level 1_Order Counts | 매도 1건수 | Int | 5 | 88 |
| Bid Level 1_Order Counts | 매수 1건수 | Int | 5 | 93 |
| Ask Level 2 price | 매도 2호가 | Double | 9 | 102 |
| Bid Level 2 price | 매수 2호가 | Double | 9 | 111 |
| Ask Level 2 volume | 매도 2잔량 | Int | 9 | 120 |
| Bid Level 2 volume | 매수 2잔량 | Int | 9 | 129 |
| Ask Level 2_Order Counts | 매도 2건수 | Int | 5 | 134 |
| Bid Level 2_Order Counts | 매수 2건수 | Int | 5 | 139 |
| Ask Level 3 price | 매도 3호가 | Double | 9 | 148 |
| Bid Level 3 price | 매수 3호가 | Double | 9 | 157 |
| Ask Level 3 volume | 매도 3잔량 | Int | 9 | 166 |
| Bid Level 3 volume | 매수 3잔량 | Int | 9 | 175 |
| Ask Level 3_Order Counts | 매도 3건수 | Int | 5 | 180 |
| Bid Level 3_Order Counts | 매수 3건수 | Int | 5 | 185 |
| Ask Level 4 price | 매도 4호가 | Double | 9 | 194 |
| Bid Level 4 price | 매수 4호가 | Double | 9 | 203 |
| Ask Level 4 volume | 매도 4잔량 | Int | 9 | 212 |
| Bid Level 4 volume | 매수 4잔량 | Int | 9 | 221 |
| Ask Level 4_Order Counts | 매도 4건수 | Int | 5 | 226 |
| Bid Level 4_Order Counts | 매수 4건수 | Int | 5 | 231 |
| Ask Level 5 price | 매도 5호가 | Double | 9 | 240 |
| Bid Level 5 price | 매수 5호가 | Double | 9 | 249 |
| Ask Level 5 volume | 매도 5잔량 | Int | 9 | 258 |
| Bid Level 5 volume | 매수 5잔량 | Int | 9 | 267 |
| Ask Level 5_Order Counts | 매도 5건수 | Int | 5 | 272 |
| Bid Level 5_Order Counts | 매수 5건수 | Int | 5 | 277 |
| Open Step Ask Total Volume | 시가 매도 총잔량 | Int | 9 | 286 |
| Open Step Bid Total Volume | 시가 매수 총잔량 | Int | 9 | 295 |
| Open Step Ask Total Counts | 시가 매도 총건수 | Int | 5 | 300 |
| Open Step Bid Total Counts | 시가 매수 총건수 | Int | 5 | 305 |
| Estimated Trading Price | 예상 체결가 | Double | 9 | 314 |
| Estimated Trading Volume | 예상 체결수량 | Int | 9 | 323 |
| End Keyword | 종료 문자 | String | 1 | 324 |
전체 메시지 길이는 324바이트입니다. 실제 메시지가 어떻게 생겼는지 보겠습니다:
1
B606F00001138G140KR4167VC0005013381075501053697000117.28000117.270000000020000000090000100004000117.29000117.260000000560000000040000700003000117.30000117.250000001170000000140000600003000117.31000117.240000001290000000120001100002000117.32000117.2300000004500000011700010000050000014840000014390042500400000000.00000000000
인터페이스 정의서에 따르면 주요 필드들의 내용은 이렇습니다:
| 필드 | 오프셋 | 길이 | 추출값 | 해석 |
|---|---|---|---|---|
| ISIN Code | 17 | 12 | KR4167VC0005 | 10년 국채 선물 (종목 코드 167) |
| Processing Time | 35 | 12 | 075501053697 | 07:55:01.053697 |
| Ask Level 1 price | 47 | 9 | 000117.28 | 117.28 |
| Bid Level 1 price | 56 | 9 | 000117.27 | 117.27 |
| Ask Level 1 volume | 65 | 9 | 000000002 | 2계약 |
| Bid Level 1 volume | 74 | 9 | 000000009 | 9계약 |
고정 길이 텍스트 포맷
눈여겨볼 점은 KRX 데이터의 모든 필드가 고정 길이 텍스트라는 점입니다. 소수점 위치도 정해져 있습니다. 참고-가격표시정보 시트를 보면 상품별 소수점 자리수가 정의되어 있습니다.
이전 글에서 정수 관리의 중요성을 다뤘죠. 그럼 수신한 가격을 먼저 정수로 파싱할 건데, 고정 자릿수라는 점을 이용해 최적화가 가능합니다.
제가 선호하는 방식은 정수 부분 전체를 한 칸 옮겨서 소수점을 없앤 뒤 정수로 변환하는 겁니다. (int)(100.05 * 100) 같은 곱셈과 캐스팅 방식은 부동소수점 오차가 생길 수 있어서 선호하지 않습니다
1
2
3
4
5
// pseudocode
input = "000100.05"
memcpy(buf, input, 6) // "000100"
memcpy(buf + 6, input + 7, 2) // "05"
result = parse_int(buf) // 10005 (원래 100.05를 소수점 2자리 기준 정수로 표현)
실제 코드는 상품별 소수점 자리수 처리, 버퍼 관리 등이 추가되어 조금 더 복잡합니다.
다른 데이터 소스처럼 소수점 위치가 고정되어 있지 않은 경우에는 정수부와 소수부를 따로 파싱해서 처리합니다:
1
2
3
4
5
6
7
8
9
10
// pseudocode
input = "123.45"
dot_pos = find('.')
integer_part = parse_int(input[0..dot_pos]) // 123
decimal_part = parse_int(input[dot_pos+1..end]) // 45
decimal_digits = 2
scale = 10^(target_decimals - decimal_digits) // 10^0 = 1
result = integer_part * 10^target_decimals + decimal_part * scale // 12345
가격 파싱 오류는 잘못된 주문으로 이어질 수 있으니까 테스트 코드를 충분히 작성해두는 게 중요합니다.
관련 글
다음 글에서는
다음 글에서는 오더북이 왜 필요한지 살펴봅니다.
