한국투자증권 API 초당 20건 제한, 70종목 처리하는 동적 배치 계산법
종목 10개로 자동매매 봇을 돌릴 땐 아무 문제가 없었어요. 그런데 포트폴리오를 70종목으로 늘리자 HTTP 429 에러가 쏟아지면서 프로그램이 멈췄습니다. 한국투자증권 Open API는 초당 20건 호출 제한이 있거든요. 고정된 배치 크기로는 종목이 늘 때마다 한계에 부딪힐 수밖에 없었어요.
이 글을 읽고 나면 종목 수에 따라 배치 크기와 대기 시간을 동적으로 계산해서 rate limit을 안전하게 지키는 방법을 코드로 구현할 수 있습니다. 실패했을 때 자동으로 재시도하는 복구 로직과 실전에서 마주친 함정까지 포함했어요.
준비물
pykis 패키지가 필요합니다. 한국투자증권 Open Trade API를 파이썬에서 쉽게 쓸 수 있게 해주는 비공식 래퍼예요. 공식 문서는 GitHub pjueon/pykis 저장소에 있습니다.
pip3 install pykis
파이썬 3.7 버전 이상이 필요해요. 저는 3.10 환경에서 테스트했습니다. KIS Developers 서비스 신청을 통해 발급받은 API 키와 앱 시크릿, 그리고 계좌 정보도 미리 준비하세요. 모의 계좌로 테스트하는 게 안전합니다. 실계좌로 바로 돌리면 예상치 못한 주문이 들어갈 수 있거든요.
API 키는 JSON 파일로 따로 관리하세요.
문제 상황 파악하기
한국투자증권 API는 초당 최대 20건 요청을 허용합니다. 70종목의 현재가를 한 번에 조회하려면 70번 호출이 필요하죠. 만약 배치 크기를 고정으로 10개씩 잡고 0.1초 간격으로 돌리면 어떻게 될까요?
10개 배치를 7번 반복하면 총 0.7초가 걸려요. 1초당 호출 수를 계산해보니 70건 ÷ 0.7초 = 초당 100건입니다. 상한의 5배를 넘기니 429 에러가 터질 수밖에 없었어요.
제가 처음 마주한 에러 메시지는 {'msg1': '초당 거래건수를 초과하였습니다'} 였습니다. 로그를 보니 43번째 호출부터 거부당했더라고요. 더 큰 문제는 에러가 나도 프로그램이 멈추지 않고 계속 요청을 보내서 IP가 일시적으로 차단될 뻔했다는 점이에요. 복구까지 5분 정도 기다려야 했습니다.
종목 수가 100개, 200개로 늘면 문제는 더 심각해집니다. 고정 배치로는 답이 없어요. 종목 수에 비례해서 배치 크기와 간격을 동적으로 조정해야 합니다.
동적 배치 크기 계산 공식 만들기
핵심은 종목 수가 변해도 초당 호출을 20건 이하로 유지하는 겁니다. 제가 쓴 공식은 이래요.
배치 크기 = min(종목 수, 초당 상한 × 안전율)
배치 간격(초) = 1초 (고정)
70종목을 예로 들면, 안전율을 90%로 잡으면 실효 상한은 초당 18건입니다. 배치 크기를 18건으로 정하면, 한 배치를 보낸 후 1초를 채우면 초당 18건을 유지할 수 있어요. 70종목은 4개 배치로 나뉘고, 총 소요 시간은 70 ÷ 18 = 약 3.9초입니다.
안전 마진을 두는 이유는 API 서버 시간 오차와 네트워크 지연 때문이에요. 딱 20건을 맞추면 타이밍에 따라 21건으로 인식될 수 있거든요. 10% 버퍼는 실전에서 거의 에러가 안 났습니다.
종목이 200개로 늘면 어떻게 바뀔까요? 배치 크기는 18개로 동일하고, 배치 간격도 1초로 동일합니다. 총 12개 배치를 처리하는 데 약 11초(200 ÷ 18)가 필요해요. 고정 배치였다면 에러 폭탄을 맞았을 겁니다.
코드로 구현하면 이렇게 됩니다.
import time
import pykis
RATE_LIMIT = 20 # 초당 최대 호출 수
SAFETY_MARGIN = 0.9 # 여유율 (90%)
def calculate_batch_params(num_stocks):
"""종목 수 기반 배치 파라미터 계산"""
effective_limit = int(RATE_LIMIT * SAFETY_MARGIN)
batch_size = min(num_stocks, effective_limit)
batch_interval = 1.0 # 배치 간격 1초 (초당 18건 유지)
return batch_size, batch_interval
# 70종목 처리
tickers = ["005930", "000660", ...] # 70개 종목 코드
batch_size, interval = calculate_batch_params(len(tickers))
print(f"배치 크기: {batch_size}, 간격: {interval:.1f}초")
# 출력 → 배치 크기: 18, 간격: 1.0초
여유율을 90%로 잡으면 실제로는 초당 18건을 쓰게 돼요. 종목이 10개뿐이면 배치 크기는 10개, 간격은 1초로 자동 조정됩니다.
배치 루프와 재시도를 한 번에 구현하기
앞서 계산한 파라미터를 실제 조회 루프에 적용하면서, 429 에러가 나도 자동 복구되도록 재시도 로직을 통합했어요. 70종목 리스트를 입력하면 딕셔너리 형태로 각 종목의 현재가가 담겨 나옵니다.
def fetch_prices_with_batch_and_retry(api, tickers, max_retries=3):
"""배치 단위로 현재가 조회 + 지수 백오프 재시도"""
batch_size, interval = calculate_batch_params(len(tickers))
prices = {}
for i in range(0, len(tickers), batch_size):
batch = tickers[i:i+batch_size]
batch_start = time.time()
for ticker in batch:
# 재시도 로직 통합
success = False
for attempt in range(max_retries):
try:
price = api.get_kr_current_price(ticker)
prices[ticker] = price
success = True
break
except Exception as e:
error_msg = str(e)
if "429" in error_msg or "초과" in error_msg:
# 지수 백오프: 0.5초 → 1초 → 2초
wait = (2 ** attempt) * 0.5
print(f"{ticker} rate limit, {wait}초 대기 (시도 {attempt+1}/{max_retries})")
time.sleep(wait)
else:
# rate limit 외 에러는 즉시 포기
print(f"{ticker} 조회 실패: {e}")
break
if not success:
prices[ticker] = None
# 다음 배치 전 대기
elapsed = time.time() - batch_start
wait_time = max(0, interval - elapsed)
if i + batch_size < len(tickers):
time.sleep(wait_time)
return prices
# 사용 예시
api = pykis.Api(key_info=key_info, account_info=account_info)
result = fetch_prices_with_batch_and_retry(api, tickers)
valid_count = len([p for p in result.values() if p])
print(f"조회 완료: {valid_count}/{len(tickers)}종목")
핵심 작동 방식은 이래요. 각 종목을 조회할 때 성공하면 다음 종목으로 넘어가고, rate limit 에러가 나면 지수 백오프로 대기합니다. 첫 번째 실패는 0.5초, 두 번째는 1초, 세 번째는 2초를 기다려요. 네트워크 단절이나 잘못된 종목 코드 같은 rate limit 외 에러는 재시도 없이 즉시 None으로 처리하고 다음 종목으로 넘어갑니다.
batch_start로 실제 소요 시간을 재서 남은 시간만큼만 대기하는 구조예요. 네트워크 응답이 느리면 대기 시간이 자동으로 줄어들어 총 실행 시간은 일정하게 유지됩니다. 마지막 배치에서는 조건문으로 sleep을 건너뛰어 불필요한 대기를 없앴고요.
조회 결과에 None이 섞여 있으면 어떻게 처리할까요? 제 경우 모멘텀 전략은 None인 종목을 건너뛰고, 리밸런싱 전략은 None이 3개 이상 나오면 전체 실행을 취소했어요. 실시간 매매는 None 비율이 20%를 넘으면 알림을 보냅니다.
흔한 실수와 해결법
interval을 1초로 하드코딩했더니 40종목부터 429가 계속 떴어요. 콘솔에 {'msg1': '초당 거래건수를 초과하였습니다'} 가 반복되면 동적 계산으로 바꾸세요. 종목 수가 늘 때마다 코드를 고치는 건 유지보수 지옥입니다.
세션 토큰 만료도 흔한 함정이에요. 저는 새벽 배치를 4시간 돌렸는데 중간에 멈춘 적이 있거든요. api.get_access_token()을 1시간마다 명시적으로 호출하면 안전합니다. pykis가 자동 갱신을 지원하지만 장시간 실행에선 수동 갱신이 확실해요.
장 시작 직후(9시~9시 5분)에는 서버 부하 때문에 평소보다 응답이 느립니다. 제 경우엔 이 시간대만 안전율을 80%로 낮춰서 초당 16건으로 제한했어요. 시간대별 안전율 조정은 config 파일로 관리하면 편합니다.
다음 스텝
이 방법은 OHLCV 데이터 수집, 잔고 조회 등 반복 호출이 필요한 모든 API에 적용할 수 있어요. 종목이 200개로 늘어나도 공식만 믿으면 됩니다. 백테스트 엔진을 만든다면 과거 데이터 수집 단계에서 이 배치 로직을 쓰세요.
자주 묻는 질문
Q. 종목이 18개보다 적으면 어떻게 되나요?
A. 배치 크기가 자동으로 종목 수로 조정됩니다. 10종목이면 배치 크기 10, 간격 1초로 설정돼요. 초당 18건보다 훨씬 여유 있게 동작하니 안전합니다.
Q. 안전율을 100%로 하면 안 되나요?
A. 서버 시간 오차나 네트워크 지연 때문에 간혹 초당 21건으로 인식될 수 있어요. 저는 실전에서 100% 설정으로 하루 평균 2~3번 429를 받았지만, 90%로 낮추니 일주일에 1번도 안 났습니다.
Q. 배치 간격을 0.5초로 줄이면 더 빨라지나요?
A. 배치 크기도 절반(9건)으로 줄여야 초당 18건을 유지할 수 있어요. 전체 처리 시간은 똑같고 배치 횟수만 2배로 늘어나니 실익이 없습니다.