주문 실행 — 매수·매도·취소, 주문을 거부당하는 가격 포맷
매수·매도·취소 TR, 지정가/시장가, 그리고 $1 경계에서 주문을 통째로 거부당하는 가격 포맷 규칙.
이제 진짜다 — 주문을 낸다. 매수·매도·취소 모두 같은 경로(POST /api/us/ordr)에 TR코드만 다르게 보낸다: 매수 ust20000, 매도 ust20001, 취소 ust20003.
매수 / 매도
def us_order(self, side, stk_cd, stex_tp, quantity, price=None, trde_tp="00"):
api_id = "ust20000" if side == "buy" else "ust20001"
is_limit = trde_tp in ("00", "26", "27", "30")
if is_limit and not price:
raise ValueError("지정가는 주문단가 필수")
body = {
"stk_cd": stk_cd,
"stex_tp": stex_tp, # ND/NY/NA
"ord_qty": str(int(quantity)),
"ord_uv": _fmt_price(price) if (is_limit and price) else "",
"trde_tp": trde_tp, # 00=지정가, 03=시장가
}
d = self.call(api_id, "/api/us/ordr", body)
return str(d.get("ord_no", "")) # 주문번호
trde_tp: 00=지정가, 03=시장가. 지정가면 ord_uv(주문단가)가 필수, 시장가면 빈 값으로 보낸다. 성공하면 주문번호(ord_no) 가 돌아온다 — 나중에 이 번호로 취소·추적한다.
⚠️ 가격 포맷의 함정 ($1 경계)
실계좌에서 주문이 통째로 거부당하게 만드는 함정이다. 키움 규격상:
$1 이상은 소수점 2자리, $1 미만은 4자리. 어기면
rc=1517로 거부된다.
def _fmt_price(price):
return f"{price:.2f}" if abs(price) >= 1.0 else f"{price:.4f}"
88.36은 "88.36", 0.5는 "0.5000". 이걸 안 지키면 아무리 로직이 완벽해도 주문 자체가 안 나간다. 문서엔 작게 적혀 있고, 실전에서 크게 터진다.
취소 (ust20003)
원주문번호(orig_ord_no)로 특정 주문을 취소한다. 실전 래퍼는 보통 미체결 목록을 먼저 조회한 뒤, 원하는 주문(또는 당일 미체결 전량)을 취소한다.
unfilled = client.us_unfilled() # 당일 미체결
for row in unfilled:
client.us_cancel(row["ord_no"], row["stk_cd"], exchange)
드라이런(dry run) — 실주문 없이 로직 검증
실계좌에 주문을 쏘기 전에, 로직이 "언제·무엇을·얼마나" 주문하려는지 로그로만 확인하는 모드가 반드시 필요하다.
def submit_order(self, order, dry_run=False):
if dry_run:
logger.info("[DRY RUN] %s %s %d주 @ %s — 전송 안 함",
order["ticker"], order["side"],
order["quantity"], order.get("price"))
return True
# ... 실제 us_order 호출
dry_run=True로 며칠 돌려보며 "내 로직이 이상한 주문을 내지 않는지" 검증한 뒤, 확신이 서면 False로 바꾼다. 이 습관 하나가 계좌를 지킨다.
주문 성공/실패를 삼키지 않기
주문은 실패할 수 있다(거부·통신오류). 실패를 조용히 넘기면 "냈다고 생각했는데 안 나간" 최악의 상황이 된다. 실전 코드는 성공 시 주문번호를 저장하고, 실패 시 명확히 로그한다.
try:
ord_no = self._client.us_order(...)
except (KiwoomRestError, ValueError) as e:
logger.error("주문 실패: %s", e)
self.last_order_id = None
return False
self.last_order_id = ord_no or None
return True
📦 주문 메서드 추가 — 모의로 실행
같은 KiwoomRestClient에 매수/매도/취소를 추가한다.
# ── KiwoomRestClient 안에 추가 ──
@staticmethod
def _fmt_price(price):
# $1 이상 2자리, 미만 4자리 — 어기면 rc=1517 로 주문 거부
return f"{price:.2f}" if abs(price) >= 1.0 else f"{price:.4f}"
def us_order(self, side, stk_cd, stex_tp, quantity, price=None, trde_tp="00"):
api_id = "ust20000" if side == "buy" else "ust20001"
is_limit = trde_tp == "00"
if is_limit and not price:
raise ValueError("지정가는 price 필수")
d = self.call(api_id, "/api/us/ordr", {
"stk_cd": stk_cd, "stex_tp": stex_tp,
"ord_qty": str(int(quantity)),
"ord_uv": self._fmt_price(price) if is_limit else "",
"trde_tp": trde_tp, # 00=지정가, 03=시장가
})
return str(d.get("ord_no", ""))
실행 — 반드시 mock: true(모의)에서 먼저:
if __name__ == "__main__":
import pathlib
cfg = json.loads(pathlib.Path("config/settings.json").read_text("utf-8"))["kiwoom_rest"]
assert cfg.get("mock", True), "⚠️ 모의(mock=true)에서 먼저 검증하세요!"
c = KiwoomRestClient(cfg["appkey"], cfg["secretkey"], mock=True)
# 현재가보다 한참 낮은 지정가 매수 → 체결 안 되고 접수만 (안전한 첫 주문 테스트)
price = c.us_current_price("QLD", "NY")
ord_no = c.us_order("buy", "QLD", "NY", 1, price=round(price * 0.5, 2))
print("주문번호:", ord_no) # 번호가 찍히면 접수 성공
팁: 현재가의 절반 같은 '체결 안 될 지정가' 로 테스트하면, 실제 체결 없이 "주문이 접수되는지"만 안전하게 확인할 수 있다. 확인 후 그 주문은 취소하면 된다.
✅ 확인
- 모의에서 실행 → 주문번호(
ord_no)가 출력되면 주문 경로 성공. rc=1517이 뜨면 가격 포맷(소수점 자릿수),거래소오류면 **stex_tp**를 점검.- 접수된 주문은 미체결 조회(
us_unfilled) 후us_cancel로 정리.
다음 편 예고
여기까지가 "주문을 넣고 확인하는" 도구다. 6편에서는 그 위에 매매 규칙(그리드) 을 얹는다 — 언제 사고 언제 팔지를 tier 포인터로 자동 결정하는 로직.
가격 포맷·거래소 구분 때문에 주문이 거부되면 return_msg(예: rc=1517)를 댓글로 남겨주세요. 이 두 개가 초반 거부의 대부분이에요.
아래 자료에서 본인에게 적용되는 최신 조건을 다시 확인하세요. 링크는 특정 상품 가입을 권하는 제휴 링크가 아닙니다.
- 키움증권 REST API 포털 ↗토큰 발급·TR 호출·미국주식 주문/조회 스펙
- Python 표준 라이브러리 · urllib.request ↗외부 의존성 없는 HTTP 호출
이 글은 교육·정보 제공 목적이며 개인별 투자자문이나 특정 금융상품의 매수·매도 권유가 아닙니다.
이 글이 포함된 외벌이 재무 워크북(PDF)을 이메일로 무료로 받아보세요. 글과 도구는 계속 무료입니다.
함께 생각해요
이 글을 읽고 생긴 질문이나 직접 실행한 경험을 나눠주세요.
댓글을 불러오는 중입니다…