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

주문 실행 — 매수·매도·취소, 주문을 거부당하는 가격 포맷

매수·매도·취소 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)를 댓글로 남겨주세요. 이 두 개가 초반 거부의 대부분이에요.

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

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

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

함께 생각해요

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

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