한국투자증권 KIS API 로 미국 프리마켓 자동 손절/익절 만들기 — 거래소 자동 캐싱부터 시간대 게이트까지
미국주식 프리마켓(ET 04:00~09:30) 시간에 보유 종목을 자동으로 손절·익절해주는 프로그램을 한국투자증권 KIS API 로 제작한 사례입니다. 한국 시간으로는 주로 새벽 5시~오후 6시 구간이라 사람이 계속 모니터링하기 어려운 시간대이고, 이런 시간대 전용 자동매매는 수요가 많은데 구현은 까다롭습니다. NASDAQ/NYSE/AMEX 세 거래소를 모두 처리해야 하고, KIS 해외주식의 특유 제약(시장가 불가, 거래소 코드 엄격 검증)을 우회해야 하거든요. 이 글에서는 실제 제작·납품 후 대응한 이슈 5건과 그 해결 패턴을 공유합니다.
- 플랫폼
- 한국투자증권 KIS Developers API (해외주식 REST + WebSocket)
- 런타임
- Python 3.10+ 64bit · tkinter/ttk GUI · PyInstaller 단일 exe
- 대상 종목
- 보유 중인 모든 미국주식 (NASDAQ / NYSE / AMEX) · 잔고 조회 결과를 자동으로 슬롯 최대 3개에 배치
- 주문 방식
- 지정가만 (KIS 해외주식 시장가 미지원) · 트리거보다 낮은 가격으로 주문해 빠른 체결 보장
- 테스트 커버리지
- 단위·시나리오·Hypothesis 프로퍼티·WCAG 대비율 포함 124개 통과
해외주식 API
+ 거래소 캐시
+ 트리거 평가
+ 1회 재시도
알림
함정 1. "해당종목정보가 없습니다" — 거래소 코드가 NASD 에 하드코딩돼 있었던 문제
의뢰인이 실거래 테스트 중 PAPL(NYSE American 상장, 서브달러 종목) 손절 트리거가 발동했는데 KIS 가 주문을 거부했습니다. 다른 NASDAQ 종목(MEHA 등)은 정상 접수됐고요. 5초마다 자동 재시도가 무한 반복되면서 로그만 쌓였습니다.
원인은 두 군데에 거래소 코드가 NASD 로 하드코딩되어 있었다는 것:
KISApi.get_balance()가OVRS_EXCG_CD="NASD"로 1회만 호출 → NASDAQ 외 거래소 종목은 잔고에 누락되거나 거래소 정보 없음place_sell_order(symbol, qty, price, exchange="NASD")의 기본값 NASD +OrderManager가 exchange 인자를 넘기지 않음 → 모든 매도 주문이 NASD 로 접수
PAPL 은 NYSE American(AMEX 분류) 상장이라 KIS 입장에서는 "이 종목은 나스닥에 없다" 로 거부 (해당종목정보가 없습니다).
이건 KIS 공식 문서만 보고는 예상하기 어려운 함정이에요. 문서에 "거래소별로 잔고가 분리돼서 반환된다" 는 설명이 없거든요.
해결: 거래소 자동 순회 + 종목→거래소 캐싱
# core/kis_api.py
class KISApi:
def __init__(self, ...):
self._symbol_exchange_cache: dict[str, str] = {}
def get_balance(self):
# NASD/NYSE/AMEX 3거래소 순회 + 종목별 거래소 매핑 캐시
holdings = []
for excg in ("NASD", "NYSE", "AMEX"):
resp = self._inquire_balance(excg)
for item in resp["output1"]:
symbol = item["ovrs_pdno"]
# 응답에 실제 거래소가 있으면 우선 사용 (보다 정확)
self._symbol_exchange_cache[symbol] = (
item.get("ovrs_excg_cd") or excg
)
holdings.append(item)
return holdings
def place_sell_order(self, symbol, qty, price, exchange=None):
# exchange=None 이면 캐시에서 자동 해결
if exchange is None:
exchange = self._symbol_exchange_cache.get(symbol, "NASD")
...
이 패턴의 장점은 monitor / order_manager / UI 시그니처를 하나도 바꾸지 않고 거래소 자동 해결을 캡슐화했다는 것.
기존 코드는 그대로 두고 KISApi 내부에서만 처리하니까 블래스트 반경이 최소예요.
신규 KIS 해외주식 프로젝트는 처음부터 이 패턴을 기본 템플릿으로 쓰면 같은 함정 안 밟습니다.
함정 2. WebSocket 대신 REST 1초 폴링을 선택한 이유
처음에 고민한 건 "실시간 가격 감시니까 당연히 WebSocket 써야지" 였는데, 실제 구현하면서 REST 1초 폴링으로 방향을 바꿨어요.
- KIS 해외주식 WebSocket 은 인증·세션 관리가 복잡하고 미국 시장 실시간 데이터 안정성이 KIS 측에서 완전하지 않음
- 잔고 조회 응답(
inquire-balance)에 현재가(now_pric2)가 포함되므로 1초마다 한 번 호출하는 것만으로 가격 감시 + 잔고 동기화 + 손익 표시를 한 번에 처리 가능 - 부수 효과로 "API: 연결됨" 상태 표시와 모니터 가격 피드가 자연스럽게 하나로 묶임
KIS 해외주식 초당 호출 한도가 1회라서 1초 폴링이 한도 딱 안쪽이고, WebSocket 특유의 연결 끊김·재연결 로직을 안 써도 되니까 오히려 안정적이에요.
kis_websocket.py 모듈은 시뮬레이션/스텁 용도로만 남겨두고 실제 운영 경로는 REST 폴링으로 통일했습니다.
함정 3. 정규장에서 프리마켓 빌드가 오발주하지 않게 — 시간대 게이트 DI 패턴
의뢰인이 "프리마켓 전용 빌드"와 "애프터마켓 전용 빌드"를 별도 상품으로 판매하길 원했어요. 근데 초기 빌드는 시간대 무관하게 조건 충족하면 주문을 발동했습니다. 이러면 정규장에 양쪽 빌드가 동시 동작하면서 의도치 않은 매도가 나갈 위험이 있죠.
해결: 시간 판별을 호출 가능한 객체(Callable)로 생성자에 주입:
# core/monitor.py
class MonitorEngine:
def __init__(
self,
...,
is_trading_time: Callable[[], bool] = is_premarket_time,
):
self._is_trading_time = is_trading_time
def update_price(self, symbol, price):
# 현재가는 언제나 갱신
self._current_prices[symbol] = price
# 주문 로직은 지정된 시간대에만 실행
if not self._is_trading_time():
if not self._off_hours_logged:
self.log.info("거래 시간 외 — 주문 로직 스킵")
self._off_hours_logged = True
return
self._off_hours_logged = False
self._evaluate_triggers(symbol, price)
이렇게 하면:
- 프리마켓 빌드:
is_trading_time=is_premarket_time(기본값) - 애프터마켓 빌드:
is_trading_time=is_afterhours_time만 주입하면 끝 — 동일 코드베이스로 변종 출시 가능 - 테스트:
is_trading_time=lambda: True로 시간 제약 우회
시간대별로 동작이 분리되어야 하는 프로젝트(야간선물, 시간외거래 등)는 처음부터 이 DI 패턴으로 설계하면 좋아요. enum 비교나 if-else 로 박으면 의뢰인이 "야간 버전도 만들어주세요" 할 때 전면 수정이 필요하거든요.
함정 4. 서브달러 종목 호가 단위 ($0.0001)
PAPL 같은 서브달러 종목은 호가 단위가 $0.0001 입니다. 일반 종목의 $0.01 와 다르죠. 기준가를 $0.01 단위로 반올림하면 거부당하거나 체결 안 되는 문제가 생깁니다.
해결: 가격대별로 호가 단위를 자동 판별:
def round_to_tick(price: float) -> float:
"""미국주식 호가 단위 자동 반올림"""
if price < 1.0:
return round(price, 4) # $0.0001 tick
else:
return round(price, 2) # $0.01 tick
그리고 손절·익절 주문 가격은 트리거 가격보다 약간 아래(매도)/위(매수) 로 설정해 빠른 체결을 보장합니다.
- 손절 트리거 가격 = 기준가 × (1 − 손절트리거% / 100)
- 손절 주문 가격 = 기준가 × (1 − 손절주문% / 100) — 트리거보다 더 낮게
- 익절도 같은 패턴을 반대 방향으로 적용
함정 5. PyInstaller 빌드가 구글 드라이브 폴더에서 실패
프로젝트 워크스페이스가 구글 드라이브 동기화 폴더(G:/내 드라이브/...)에 있었는데, python build.py 를 돌리면 다음 에러가 났어요:
OSError: [WinError 5] 액세스가 거부되었습니다. EndUpdateResourceW
원인은 PyInstaller 가 exe 리소스 섹션을 갱신하는 순간 구글 드라이브 클라이언트가 파일을 동기화 락으로 잡고 있어서 EndUpdateResourceW 호출이 거부되는 거예요.
OneDrive, Dropbox 도 같은 이슈 있습니다.
해결: 로컬 temp 경유 빌드
# build.bat
mkdir -p "C:/Users/.../AppData/Local/Temp/kis_build"
cd "C:/Users/.../AppData/Local/Temp/kis_build"
python -m PyInstaller \
--distpath=dist --workpath=build --specpath=spec \
--add-data="<gdrive>/design_tokens.py;." \
--paths="<gdrive>" \
"<gdrive>/main.py"
cp dist/KIS_PreMarket.exe "<gdrive>/release/"
규칙 하나만 기억하면 돼요: 클라우드 동기화 폴더에서는 절대 PyInstaller 를 직접 돌리지 말 것. 항상 로컬 temp 경유.
그 외 실전에서 챙긴 것들
보유 종목 자동 슬롯 배치
사용자가 매수만 하면 자동으로 빈 슬롯에 평단가·기본 %가 채워지도록. 전량 매도된 종목은 슬롯 자동 비우고 기본 설정 복원. "기본값 1회 설정" + "매수만 하면" 자동매도까지 흐름이 완료되는 UX.
드라이런 모드 + 시간 게이트 = 이중 안전망
config.dry_run = True 가 기본값이고, place_sell_order 첫 줄이 if config.dry_run: raise KISApiError(code="DRY_RUN_GUARD"). 두 번째 방어선입니다.
의도치 않은 실거래를 막기 위해 "기본값 드라이런 → 사용자가 의도적으로 실거래 모드 켜기" 흐름.
tkinter + 디자인 토큰 + WCAG AA 자동 검증
GUI 가 PyQt5 처럼 무거울 필요 없어서 tkinter/ttk 로 경량 구현.
design_tokens.py 에 다크/라이트 색상 정의하고 contrast_ratio.py 가 모든 텍스트/배경 조합의 대비율을 자동 계산해서 4.5:1 미달이면 테스트가 차단합니다.
디자인 시스템이 회귀 없이 유지되는 안전장치죠.
reportlab 한글 PDF 매뉴얼
reportlab 기본 폰트는 한글 미지원이라 C:/Windows/Fonts/malgun.ttf, malgunbd.ttf 를 직접 등록해서 한글 PDF 사용설명서를 자동 생성.
빌드 시 build_pdf_manual.py 가 실제 paths.py 등 코드에서 데이터 경로·핫키·설정 키를 추출해서 매뉴얼에 넣도록 했어요 — 매뉴얼과 코드가 안 맞아서 고객 지원 들어가는 상황 방지.
KIS 해외주식 자동매매를 직접 만들지 말지 결정 기준
직접 만드는 게 낫다면
- Python 64bit 경험 + REST API 경험이 있고
- KIS 공식 문서 + OAuth 토큰 관리 + 서명 로직을 스스로 뚫을 시간이 있고
- 한두 종목 고정이라 거래소 자동 해결이 필요 없고
- GUI·알림·.exe 배포 등 부가기능이 필요 없음
외주가 유리하다면
- 보유 종목 자동 감지 + NASD/NYSE/AMEX 혼재 처리 필요
- 프리마켓·정규장·애프터마켓 여러 시간대 분리 운영 계획
- GUI 설정창 + tkinter 다크테마 + PyInstaller .exe 배포 필요
- 한글 PDF 사용설명서·WCAG 대비율 같은 품질 기준 원함
- 위 5가지 함정 전부 처음부터 피해서 가고 싶음
참고: KIS 해외주식 API 는 국내주식 API 보다 제약이 훨씬 많아요. 특히 해외 시장가 주문 불가(지정가만 가능), 초당 1회 호출 한도, 거래소별 잔고 분리 반환 이 세 가지를 모르고 시작하면 중간에 전면 재설계 필요합니다. 더 자세한 기술 문서는 한국투자증권 KIS API 플랫폼 페이지를 참고하세요.
비슷한 프로젝트 필요하신가요?
KIS·키움·LS·업비트·바이낸스 등 3년 동안 359건 이상 제작한 알고랩이 맞춤 제작해드립니다.
24시간 빠른 답변 가능합니다.