키움 API 를 이용한 자동매매 프로그램 개발 (삽질일기), KIWOOM REST API AUTO TRADING BOT

Python Kiwoom REST API 알고리즘 트레이딩

키움 REST API로 그리드 자동매매 봇 만들기 — 삽질 기록

v2.0부터 v3.4까지, 3주간의 개발 과정에서 겪은 오류와 해결법을 정리합니다.

2026-04-02 · TIGER KOSDAQ150 · Python 3 · Kiwoom REST API

목차
  1. 프로젝트 개요 및 아키텍처
  2. 핵심 매매 전략 — 그리드 + 연속 스트릭
  3. 삽질 1: API 파라미터가 문서랑 달랐다
  4. 삽질 2: Rate Limit — HTTP 429의 습격
  5. 삽질 3: 페이지네이션 없이 체결내역 집계했더니 수익이 반토막
  6. 삽질 4: 오버나이트 토큰 만료 — 매일 아침 봇이 죽는 이유
  7. 상태 복구 전략의 변천사
  8. 마무리: 지금 돌아가고 있는 구조

1. 프로젝트 개요 및 아키텍처

TIGER KOSDAQ150 ETF를 대상으로 그리드 방식의 자동매매 봇을 만들었다. 목표는 단순하다: 기준가 위아래에 매수/매도 주문을 4개 깔아두고, 체결이 발생하면 즉시 재정비한다.

v2.0에서 완전히 새로 짠 구조는 다음과 같다.

config.py
중앙 설정, .env 로드
kiwoom_api.py
REST API 클라이언트
trader.py
핵심 매매 로직
main.py
진입점, 대시보드 서버
cancel_all.py
긴급 주문 취소 도구
.env
API 자격증명 (gitignore)

API 인증은 Bearer 토큰 방식이라 acnt_no / acnt_pwd를 매 요청마다 넘길 필요가 없었다. 시뮬레이션(모의투자)은 헤더에 simulation-yn: "Y"만 추가하면 된다.


2. 핵심 매매 전략 — 그리드 + 연속 스트릭

10초마다 폴링하며 아래 사이클을 반복한다.

매 사이클 (10s):
  1. ka10075로 미체결 주문 수 조회
  2. 주문 수 != 4이면:
     a. 어떤 주문이 체결됐는지 확인
     b. 방향(direction) + 연속 스트릭(streak) + 보유수량 업데이트
     c. 잔여 주문 전량 취소
     d. ka10006으로 현재가 조회
     e. 4개 신규 주문 배치
  3. 주문 수 == 4이면: 대기

그리드 간격은 고정 원화 방식으로 최종 정착했다. (스프레드 퍼센트 → 고정 원 방식으로 두 번 바뀌었다. 그 이유는 아래에서 설명한다.)

sell2 @ base + 100 * streak
sell1 @ base + 50  * streak
─────────── base ───────────
buy1  @ base - 50  * streak
buy2  @ base - 100 * streak

스트릭이 올라갈수록 그리드 폭이 넓어진다. 같은 방향으로 계속 체결되면 추세가 있다고 판단해 스프레드를 벌리는 원리다.


3. 삽질 1: API 파라미터가 문서랑 달랐다 v2.0

키움 REST API를 처음 붙일 때 가장 많이 헤맨 부분이다. 공식 문서와 실제 동작이 다른 케이스들을 정리한다.

⚠️ 주문 가격 파라미터는 ord_prc가 아니라 ord_uv다. 문서에는 ord_prc로 나온 경우도 있으니 주의.

# kt10000 — 매수 주문
{
  "stk_cd":       "069500",
  "ord_qty":      5,
  "ord_uv":       19710,   # ord_prc 아님!
  "ord_dv":       "00",
  "trde_tp":      "0",
  "stex_tp":      "KRX",
  "dmst_stex_tp": "KRX"    # 이거 빠지면 오류
}

추가로 확인한 필수 파라미터 규칙들:

  • dmst_stex_tp: "KRX" — 없으면 요청 자체가 실패한다
  • 가격 틱은 5원 단위. 5원 단위가 아니면 주문 거부
  • 공매도 불가 — 보유수량 부족 시 매도 주문 넣으면 에러. 코드에서 직접 가드 처리 필요
  • WebSocket은 mockapi에서 500 반환 → REST 폴링으로 전환

4. 삽질 2: Rate Limit — HTTP 429의 습격 v2.4 → v2.7

초기엔 주문/취소 사이에 딜레이를 각 함수에서 따로 관리했다. 스타트업 시퀀스에서 cancel_all이 끝나자마자 poll_once가 실행되면서 ka10006을 너무 빠르게 연속 호출해 429가 터졌다.

⚠️ 같은 엔드포인트를 복수 함수에서 호출할 때, 각자 딜레이를 관리하면 레이트 리밋 추적이 불가능해진다.

해결: 레이트 리밋을 _post() 레이어로 집중

def _post(self, path, body, extra_headers=None):
    resp = requests.post(...)
    time.sleep(1.2)   # 모든 API 호출에 공통 적용
    return resp.json()

이후 trader.pymain.py에서 time.sleep() 호출을 전부 제거했다. API 레이어가 속도를 책임지니 비즈니스 로직이 훨씬 깔끔해졌다.


5. 삽질 3: 페이지네이션 없이 수익 집계했더니 반토막 v3.1

하루 체결내역을 kt00007로 가져와 FIFO 방식으로 수익을 계산했는데, 실제 수익과 계속 차이가 났다.

⚠️ kt00007은 한 번에 전체 데이터를 주지 않는다. 페이지네이션 없이 첫 페이지만 읽으면 당일 체결의 일부만 집계된다.

해결: cont-yn / next-key 헤더로 전 페이지 병합

def get_completed_orders(self):
    all_items = []
    next_key = None
    while True:
        data, headers = self._post_paged("/api/dostk/acnt", body, next_key)
        all_items.extend(data.get("items", []))
        if headers.get("cont-yn") != "Y":
            break
        next_key = headers.get("next-key")
    return merge_into_single_response(all_items)

수익 계산 로직은 FIFO 페어링 방식이다. ord_no 오름차순 정렬 후 가장 오래된 매수와 매도를 순서대로 매칭한다.


6. 삽질 4: 오버나이트 토큰 만료 — 매일 아침 봇이 죽는 이유 v3.4

장 마감 후 봇은 슬립 루프에 들어간다. 다음날 아침 장 시작 시각에 깨어나서 매매를 재개하는데, 며칠 후 이상한 패턴이 발견됐다.

[1] price unavailable — skip
[1] price unavailable — skip
[1] price unavailable — skip
...

⚠️ 키움 OAuth 토큰의 유효기간은 하루다. 프로세스 시작 시점에 발급된 토큰이 오버나이트 슬립 중에 만료되면, 다음날 아침 kt00007과 ka10006이 에러코드 8005를 반환하며 조용히 실패한다.

문제는 API가 예외를 던지지 않고 에러코드를 payload에 담아 반환한다는 점이다. 처리가 없으면 그냥 "가격 없음"으로 넘어가버린다.

해결: 세션 시작 시 토큰 갱신

def startup():
    # 매일 장 시작 전 토큰 재발급
    api.get_token()
    trader.write_status()
    ...

main()의 슬립/웨이크업 루프에 텔레그램 알림도 추가했다. 봇이 언제 자고 깨는지 모니터링할 수 있다.


7. 상태 복구 전략의 변천사

프로세스가 재시작됐을 때 이전 상태(마지막 체결가, 스트릭, 방향)를 어떻게 복구하느냐가 꽤 고민됐다. 아래 순서로 전략이 바뀌었다.

ℹ️ v2.3: saved.csv에 상태를 저장하고 재시작 시 읽어옴. stock_code 불일치 시 무시.

⚠️ v2.6: saved.csv 제거. 파일 기반 상태 관리의 엣지케이스가 복잡해 전량 삭제.

v3.0: kt00007(당일 체결내역)을 우선 조회 → 마지막 체결가와 방향 복구. 체결 없으면 ka10006 현재가 사용, 스트릭은 1로 리셋.

결론적으로 파일보다 API에서 직접 상태를 복구하는 것이 더 안정적이었다. 파일은 동기화 타이밍 문제가 생기지만, API는 항상 최신 상태를 반환한다.


8. 마무리: 지금 돌아가고 있는 구조

v3.4 기준 현재 안정적으로 운영 중인 구조다.

  • 10초 폴링 → 미체결 수 != 4이면 전량 취소 후 재배치
  • 매일 장 시작 전 OAuth 토큰 재발급
  • 체결내역 전 페이지 수집 후 FIFO 수익 계산
  • 상태 복구는 kt00007 우선, 없으면 ka10006 현재가
  • 모든 API 호출은 _post()에서 1.2s 딜레이 보장
  • status.json을 매 사이클 갱신, 로컬 HTTP 서버로 대시보드 제공
  • 슬립/웨이크업 시 텔레그램 알림

✅ 아직 미확인된 엔드포인트(ka10076 거래내역)가 남아 있다. 파라미터가 확정되면 상태 복구 우선순위에 추가할 예정이다.

댓글

이 블로그의 인기 게시물

로컬 Gemma를 Claude Code 백엔드로 쓰기

gitea / ubuntu linux 개발 환경 구축기