파이썬으로 자동매매 만들기 · 4편

미국주식 시세·잔고·체결 조회 — 그리고 '원장일'의 함정

미국주식 현재가·잔고·체결내역 조회. 한국 자정에 안 바뀌는 '원장일'과 체결 누락을 막는 catch-up.

토큰이 준비됐으니, 이제 매매 판단에 필요한 데이터를 읽어온다 — 현재가·잔고·체결내역. 전부 미국주식 전용 TR(/api/us/...)이다.

현재가 (usa20100)

종목코드와 거래소 구분(stex_tp)을 함께 보낸다. ND=나스닥, NY=NYSE, NA=아멕스.

def us_current_price(self, stk_cd, stex_tp):
    d = self.call("usa20100", "/api/us/mrkcond",
                  {"stk_cd": stk_cd, "stex_tp": stex_tp})
    raw = d.get("cur_prc", "")           # "+88.3600" 처럼 부호가 붙어 온다
    return abs(float(raw)) if raw else None

함정: cur_prc는 등락 부호가 붙어서("+88.36") 온다. abs(float(...))로 가격만 빼내야 한다.

거래소를 매번 지정하기 번거로우면, 한 번 탐지해서 캐시하면 된다. ND→NY→NA 순서로 현재가를 시도해보고, 값이 나오는 거래소를 기억한다. (운영 종목은 그래도 exchange_map에 명시하는 게 안전하다 — 거래소가 틀리면 주문이 거부된다.)

잔고 (ust21070) · 예수금 (ust21160)

계좌번호를 넣을 필요가 없다(appkey에 연동). 응답의 result_list[]에 보유 종목이 들어온다.

def us_position_qty(self, stk_cd):
    for row in self.us_balance().get("result_list", []):
        if row.get("stk_cd") == stk_cd:
            return int(row.get("poss_qty", "0") or 0)   # 보유수량
    return 0

주요 필드: poss_qty(보유수량), sell_alowq(매도가능), frgn_stk_book_uv(평균단가), now_pric(현재가), evlt_amt(평가금액), pl_amt/pl_rt(손익/손익률). 예수금은 ust21160의 d0_usd_fx_entr(USD 외화예수금).

체결내역 (ust21150) — 그리고 "원장일"

여기가 초보자가 가장 많이 틀리는 지점이다.

미국 장은 한국시간 밤 22:30~새벽 05:00에 열린다. 즉 한국 날짜로는 자정을 넘긴다. 만약 우리가 "오늘 날짜"로 체결을 조회하면, 자정 전 체결과 자정 후 체결이 다른 날짜로 갈려 절반을 놓친다.

키움은 이걸 원장일(미국 거래일 단위) 로 처리한다. 원장일은 한국 자정에 바뀌지 않는다. 그래서 조회할 때 날짜를 우리가 계산하지 말고, 빈 값으로 서버에 맡기는 게 안전하다.

# 기본(오늘 원장일): ord_dt 를 빈 문자열로 → 서버가 알아서 처리
rows = self._client.us_executions(ord_dt="")

catch-up(따라잡기): 봇이 밤사이 멈췄다가 다음 날 켜지면, 직전 원장일 체결을 놓친다. 그래서 "오늘 + 직전 N일"을 합쳐 조회하고 주문번호(ord_no)로 중복 제거한다.

# 지난 며칠치까지 합쳐 조회 (재시작 복구용)
history = controller.get_execution_history(lookback_days=3)

그리고 완전 체결만 잡는다 — 잔량(ord_remnq)이 0이고 체결량(cntr_qty)이 0보다 클 때. (부분체결은 전량 체결된 뒤 같은 주문번호로 한 번 잡힌다.)

미체결 (ust21050)

아직 안 붙은 주문. ord_remnq(잔량)가 0보다 큰 행만 유효하다. 취소나 상태 확인에 쓴다.

조회는 "실패해도 멈추지 않게" (fail-open)

시세·잔고 조회가 잠깐 실패한다고 봇이 죽으면 안 된다. 실전 코드는 조회 실패 시 에러를 던지지 않고 None/빈 리스트를 돌려주고, 그 사이클만 건너뛴다.

try:
    return _result_list(self.call("ust21150", "/api/us/acnt", body))
except KiwoomRestError as e:
    _log_query_failure("us_executions", e)   # '결과 없음'(rc=20)은 debug 로만
    return []                                 # 멈추지 않는다

📦 클라이언트에 조회 메서드 추가 — 실행

3편의 KiwoomRestClient 클래스 안에 아래 메서드를 추가하자.

    # ── hero/kiwoom_rest.py 의 KiwoomRestClient 안에 추가 ──

    def us_current_price(self, stk_cd, stex_tp):
        d = self.call("usa20100", "/api/us/mrkcond",
                      {"stk_cd": stk_cd, "stex_tp": stex_tp})
        raw = d.get("cur_prc", "")               # "+88.3600" 형태
        return abs(float(raw)) if raw else None

    def us_balance(self):
        return self.call("ust21070", "/api/us/acnt", {})

    def us_executions(self, ord_dt=""):
        body = {"ord_dt": ord_dt, "query_tp": "5", "slby_tp": "0",
                "stex_tp": "", "stk_cd": "", "oppo_trde_tp": "", "fr_ord_no": ""}
        d = self.call("ust21150", "/api/us/acnt", body)
        return d.get("result_list") or d.get("result_lsit") or []

실행 스크립트(3편의 __main__을 이렇게 바꿔서):

if __name__ == "__main__":
    import pathlib
    cfg = json.loads(pathlib.Path("config/settings.json").read_text("utf-8"))["kiwoom_rest"]
    c = KiwoomRestClient(cfg["appkey"], cfg["secretkey"], mock=cfg.get("mock", True))

    print("QLD 현재가:", c.us_current_price("QLD", "NY"))   # 예: 88.36

    for row in c.us_balance().get("result_list", []):
        print(f"보유: {row['stk_cd']} {row.get('poss_qty')}주 "
              f"@ {row.get('frgn_stk_book_uv')}")

    for f in c.us_executions():        # 오늘 원장일 체결
        print("체결:", f.get("stk_cd"), f.get("slby_tp_nm"),
              f.get("cntr_qty"), "@", f.get("cntr_uv"))

✅ 확인

python hero/kiwoom_rest.py
  • 현재가가 숫자로 출력되면 시세 조회 성공. (거래소 stex_tp가 틀리면 None이 나오니, QLD는 NY, NVDA는 ND처럼 맞춰보자.)
  • 보유 종목이 있으면 수량·평단이 출력된다. (없으면 빈 줄 — 정상)
  • 체결내역은 오늘 원장일 기준. 오늘 매매가 없으면 안 나오는 게 정상이다.

다음 편 예고

5편에서는 드디어 주문을 낸다 — 매수·매도·취소. 그리고 실계좌에서 주문을 통째로 거부당하게 만드는 가격 포맷의 함정($1 경계에서 소수점 자릿수가 갈린다)을 짚습니다.


원장일 개념이 헷갈리면 댓글로 물어봐 주세요 — 자동매매에서 체결 누락은 치명적이라, 여기만 정확히 잡아도 절반은 한 거예요.

핵심 주장 확인 자료 · 검토 기준 2026-08-27

아래 자료에서 본인에게 적용되는 최신 조건을 다시 확인하세요. 링크는 특정 상품 가입을 권하는 제휴 링크가 아닙니다.

이 글은 교육·정보 제공 목적이며 개인별 투자자문이나 특정 금융상품의 매수·매도 권유가 아닙니다.

함께 생각해요

이 글을 읽고 생긴 질문이나 직접 실행한 경험을 나눠주세요.

댓글을 불러오는 중입니다…