AlgoLab Blog · 업비트 주문 실무 · 2026

업비트 주문 정정 — cancel_and_new 함정 5가지

업비트 · 주문 API 2026-08-27 · 약 8분 읽기 · 알고랩 AlgoLab
한 줄 요약 업비트 Open API에는 주문 정정 엔드포인트가 없습니다. 가격이나 수량을 바꾸려면 POST /v1/orders/cancel_and_new(취소 후 재주문) 하나를 씁니다. 이 호출은 같은 페어·같은 방향으로만 다시 넣을 수 있고, 신규 주문은 이전 주문의 취소가 완료된 뒤에 생성되며 새 uuid를 받습니다. 개별 취소는 DELETE /v1/order, 미체결 일괄 취소는 DELETE /v1/orders/open이고 세 호출의 요청 수 제한 그룹이 서로 다릅니다.

지정가 매수를 걸어 놨는데 호가가 달아나서 가격만 올려 다시 걸고 싶은 상황. 한국투자증권이나 키움처럼 정정 전용 TR이 있는 구조에 익숙하면 업비트 문서에서 "정정"을 한참 찾게 됩니다. 없습니다. 그리고 대체품인 cancel_and_new단순한 취소 + 주문의 합이 아닙니다 — 제약이 붙어 있고, 부분체결 처리 방식이 따로 있고, 호출 한도 그룹도 다릅니다. 아래는 2026-08-27 확인 기준 업비트 개발자 문서(docs.upbit.com)의 주문 계열 레퍼런스와 요청 수 제한 정책 문서를 대조해 정리한 사고 지점 5가지입니다. JWT 인증 자체는 업비트 REST·WebSocket 실전 정리에서 다뤘습니다.

이 글에서 다루는 것

  1. 업비트 주문 취소 계열 API 4개 지도
  2. 함정 1 — 정정은 없고, 방향도 못 바꾼다
  3. 함정 2 — 부분체결 잔량은 remain_only로 넘긴다
  4. 함정 3 — identifier는 재사용할 수 없다
  5. 함정 4 — 호출 한도 그룹이 세 개로 쪼개져 있다
  6. 함정 5 — 일괄 취소는 WATCH 주문을 못 지운다
  7. 봇에 넣는 재주문 루틴 (전체 코드)

1. 업비트 주문 취소 계열 API 4개 지도

먼저 전체 그림입니다. 업비트에서 이미 낸 주문을 건드리는 API는 네 개이고, 각각 HTTP 메서드와 파라미터 전달 방식이 다릅니다.

용도메서드 · 경로핵심 파라미터요청 수 제한 그룹
개별 취소DELETE /v1/orderuuid 또는 identifierExchange 기본 · 초당 30회
취소 후 재주문POST /v1/orders/cancel_and_newprev_order_uuid, new_ord_type, new_volume, new_priceExchange 주문 · 초당 12회
미체결 일괄 취소DELETE /v1/orders/opencancel_side, count, pairs, quote_currenciesExchange 일괄취소 · 2초당 1회
ID로 상태 확인GET /v1/orders/uuidsuuids[] 또는 identifiers[] (최대 100)Exchange 기본 · 초당 30회

네 개 모두 [주문하기] 권한이 있는 API 키가 필요합니다. 조회 권한만 있는 키로는 인증 단계에서 막힙니다. 키 권한을 나누는 원칙은 API 키 보안과 계좌 보호에 있습니다.

증권사 API (KIS · 키움) 원주문 정정 TR 원주문번호 유지 같은 주문, 새 가격 업비트 cancel_and_new 원주문 uuid A 취소 완료 그다음에야 신규 새 주문 new_order_uuid B ※ 주문 식별자가 바뀌므로, uuid를 키로 상태를 들고 있던 봇은 추적이 끊깁니다.
정정 TR이 있는 증권사 API vs 업비트의 취소 후 재주문

2. 함정 1 — 정정은 없고, 방향도 못 바꾼다

cancel_and_new는 이름이 전부입니다. 취소하고, 새로 넣습니다. 업비트 문서는 "신규 주문은 기존 주문과 동일한 페어, 동일한 주문 방향에 대해서만 생성 가능"하며 변경할 수 없다고 못 박고 있습니다. 즉 marketside는 건드릴 수 없습니다.

바꿀 수 있는 것은 아래 다섯 개뿐입니다.

파라미터의미
new_ord_typelimit / price / market / best주문 유형 (필수)
new_volume수량 또는 remain_only주문 유형에 따라 조건부 필수
new_price단가 또는 총액주문 유형에 따라 조건부 필수
new_time_in_forcefok / ioc / post_only체결 조건
new_smp_typecancel_maker / cancel_taker / reduce자전거래 방지 모드

취소 대상은 prev_order_uuid 또는 prev_order_identifier하나를 반드시 넣어야 합니다. 문서상 둘 다 선택 파라미터로 표기돼 있지만, 어느 쪽도 없으면 대상 주문을 특정할 수 없으니 사실상 필수입니다.

# 지정가 매수 주문의 가격만 올려서 다시 거는 요청 본문
{
  "prev_order_uuid": "cdd92199-2897-4e14-9448-f923320408ad",
  "new_ord_type": "limit",
  "new_volume": "remain_only",
  "new_price": "104500000"
}

POST 요청은 JSON으로 보내야 합니다. 업비트는 주문 계열 POST에서 Form·Urlencoded 형식을 더 이상 지원하지 않습니다. 파이썬 requests로 짤 때 data=가 아니라 json=을 써야 하고, JWT 서명에 쓰는 해시 대상도 그에 맞춰야 합니다. 습관적으로 data=를 쓰면 400이 돌아옵니다.

방향을 뒤집어야 한다면 cancel_and_new가 아니라 DELETE /v1/order로 지우고 POST /v1/orders로 새로 내는 두 단계입니다. 주문 유형 자체를 어떻게 고를지는 시장가·지정가·조건부 주문 정리를 참고하십시오.

3. 함정 2 — 부분체결 잔량은 remain_only로 넘긴다

10개를 걸었는데 3개가 체결된 상태에서 가격을 바꾸고 싶다고 합시다. 보통은 이렇게 짭니다.

# 흔한 실수 — 잔량을 봇이 직접 계산한다
order = get_order(uuid)                       # GET /v1/order
remain = Decimal(order["remaining_volume"])   # 이 시점 잔량 7
cancel_and_new(prev_order_uuid=uuid,
               new_ord_type="limit",
               new_volume=str(remain),        # 여기서 이미 낡은 값일 수 있다
               new_price="104500000")

조회와 재주문 사이에 체결이 더 붙으면 remaining_volume은 이미 옛날 값입니다. 업비트는 이 경합을 피하라고 new_volumeremain_only라는 문자열을 받도록 해 뒀습니다.

"new_volume": "remain_only" — 숫자 대신 이 문자열을 넣으면 기존 주문의 잔여 수량이 그대로 신규 주문 수량이 됩니다. 봇이 잔량을 읽고 빼는 두 번의 왕복이 사라지고, 그 사이의 체결 경합도 서버 쪽에서 정리됩니다.

응답에는 원주문과 신규 주문이 둘 다 들어옵니다. uuid는 취소된 원주문이고, new_order_uuid가 새로 생긴 주문입니다. 이걸 헷갈려 uuid를 계속 추적하면 봇은 영원히 "취소됨"만 보게 됩니다.

{
  "uuid": "cdd92199-2897-4e14-9448-f923320408ad",
  "new_order_uuid": "b1f3c8a0-4d51-4a2e-9f77-0c9a1d2e3b44",
  "new_order_identifier": null,
  "market": "KRW-BTC",
  "side": "bid",
  "ord_type": "limit",
  "price": "104500000",
  "state": "wait",
  "volume": "0.007",
  "remaining_volume": "0.007",
  "executed_volume": "0",
  "locked": "731500.0",
  "prevented_volume": "0",
  "prevented_locked": "0",
  "created_at": "2026-08-27T10:14:22+09:00"
}

prevented_volume·prevented_locked자전거래 방지(SMP)로 취소된 수량과 풀린 금액입니다. 평소엔 0이지만 같은 계정으로 매수·매도를 동시에 돌리면 여기에 값이 찍히면서 실제 체결 수량이 기대와 달라집니다.

4. 함정 3 — identifier는 재사용할 수 없다

업비트는 주문마다 클라이언트가 직접 붙이는 식별자 identifier를 지원합니다. 네트워크가 끊겨 응답을 못 받았을 때 중복 주문을 막는 멱등키로 쓰기 좋습니다. 그런데 cancel_and_new에는 명시적인 제약이 하나 붙습니다.

취소하려는 기존 주문에 사용된 identifier 값은 다시 쓸 수 없습니다. new_identifier에 원래 값을 그대로 넣는 설계는 처음부터 막혀 있습니다.

"주문 하나 = identifier 하나"로 잡아 둔 봇은 재주문 순간 설계가 깨집니다. 해법은 전략 단위 키와 주문 단위 키를 분리하는 것입니다.

# 전략 키는 고정, 주문 키는 재주문마다 증가
strategy_key = "btc-grid-03"          # 봇 내부에서 포지션을 묶는 키
attempt      = 2                       # 이 슬롯의 몇 번째 재주문인가
new_identifier = f"{strategy_key}-{attempt:03d}"   # btc-grid-03-002

payload = {
    "prev_order_uuid": prev_uuid,
    "new_ord_type": "limit",
    "new_volume": "remain_only",
    "new_price": str(new_price),
    "new_identifier": new_identifier,
}

개별 취소인 DELETE /v1/orderuuididentifier 중 하나로 지정하는데, 둘 다 넣으면 uuid가 우선합니다.

그리고 이 요청은 파라미터를 쿼리로만 받습니다. 본문에 실으면 이렇게 돌아옵니다.

HTTP/1.1 400 Bad Request

{"error":{"name":"QUERY_PARAMETER_NOT_SUPPORTED",
          "message":"요청 본문(body)으로 전달된 파라미터는 지원하지 않습니다."}}

이미 취소됐거나 존재하지 않는 주문을 다시 지우면 404와 함께 order_not_found가 옵니다. 재시도 로직에서 이 코드는 실패가 아니라 "이미 목표 상태"로 처리해야 무한 재시도에 빠지지 않습니다.

5. 함정 4 — 호출 한도 그룹이 세 개로 쪼개져 있다

업비트의 요청 수 제한은 하나의 숫자가 아니라 그룹별로 걸립니다. 2026-08-27 확인 기준 공식 요청 수 제한 정책 문서의 값은 아래와 같습니다.

그룹한도기준해당 호출
Quotation (market·candle·trade·ticker·orderbook)각 초당 10회IP시세·캔들·호가 조회
Exchange 기본초당 30회계정(포켓)잔고, 개별 주문 조회·취소, 입출금
Exchange 주문초당 12회계정(포켓)POST /v1/orders, cancel_and_new
Exchange 일괄취소2초당 1회계정(포켓)DELETE /v1/orders/open
WebSocket 연결초당 5회IP 또는 계정연결 요청
WebSocket 메시지초당 5회 · 분당 100회연결데이터 요청 메시지

일괄 취소가 2초당 1회라는 점이 특히 중요합니다. "전부 취소하고 다시 짠다"를 반복하는 전략은 여기서 가장 먼저 걸리고, 초당 30회짜리 개별 취소를 30번 돌리는 편이 더 빠른 경우도 생깁니다.

남은 횟수는 응답 헤더로 옵니다.

Remaining-Req: group=default; min=1800; sec=29

group이 지금 요청이 속한 그룹, sec이번 초의 남은 횟수입니다. min은 분 단위 제한이 폐지되면서 고정값으로 돌아오는 더 이상 참고하지 않는 필드이니 무시하십시오. sec가 0이면 다음 초 경계까지 기다렸다가 재시도합니다.

import time

def respect_limit(resp):
    """Remaining-Req 헤더를 읽어 남은 횟수가 없으면 다음 초까지 쉰다."""
    raw = resp.headers.get("Remaining-Req", "")
    parts = dict(p.strip().split("=", 1) for p in raw.split(";") if "=" in p)
    group = parts.get("group", "?")
    sec = int(parts.get("sec", 1))
    if sec <= 0:
        # 초 경계까지만 대기 — 고정 sleep보다 훨씬 덜 버린다
        time.sleep(1.0 - (time.time() % 1.0))
    return group, sec

한도를 넘기면 429 Too Many Requests가 옵니다. 여기서 멈추지 않고 계속 때리면 418로 올라가고 차단 시간이 길어집니다. 429를 받은 뒤 즉시 재시도하는 코드가 가장 위험합니다. 백오프 설계 원칙은 API 호출 한도 설계에, 다른 거래소의 같은 문제는 바이낸스 weight와 IP 밴에 정리해 두었습니다.

6. 함정 5 — 일괄 취소는 WATCH 주문을 못 지운다

DELETE /v1/orders/open은 조건에 맞는 미체결 주문을 최대 300건까지 한 번에 지웁니다. 파라미터는 전부 쿼리로 넣습니다.

파라미터기본값제한
cancel_sideallbid / ask / all
count20최대 300
order_bydescasc(오래된 순) / desc
pairs최대 20개, 콤마 구분
excluded_pairs최대 20개, 콤마 구분
quote_currenciesKRW · BTC · USDT

pairsquote_currencies동시에 쓸 수 없습니다. "KRW 마켓 전체에서 매수만 취소"는 되지만, "KRW 마켓 중 이 세 종목만"은 한 번에 안 됩니다.

DELETE /v1/orders/open?cancel_side=bid&quote_currencies=KRW&count=300

응답은 성공과 실패를 나눠 줍니다. 전부 지워졌다고 가정하면 안 됩니다.

{
  "success": { "count": 18,
    "orders": [ {"uuid":"...","market":"KRW-BTC","identifier":null} ] },
  "failed":  { "count": 2,
    "orders": [ {"uuid":"...","market":"KRW-ETH","identifier":null} ] }
}

예약 상태(WATCH)인 주문은 일괄 취소로 지워지지 않습니다. 스탑 계열 예약 주문을 걸어 둔 상태에서 "전부 취소"를 돌리면 대기(WAIT) 주문만 사라지고 예약 주문은 살아남습니다. 봇 입장에서는 다 정리한 줄 알았는데 나중에 혼자 체결되는 최악의 시나리오입니다. 예약 주문은 DELETE /v1/order로 개별 취소하거나 ID 지정 취소를 따로 돌려야 합니다.

그래서 실전 봇은 일괄 취소 뒤에 반드시 확인 호출을 붙입니다. GET /v1/orders/uuidsuuids[] 또는 identifiers[]최대 100건의 상태를 한 번에 확인합니다. 둘을 동시에 넣을 수는 없습니다.

state의미봇이 해야 할 일
wait체결 대기아직 살아 있음 — 다시 취소
watch예약 주문 대기일괄 취소로 안 지워짐 — 개별 취소
done전체 체결 종료취소 불가 — 포지션에 반영
cancel취소executed_volume 확인 후 정리

취소됐다고 체결이 없었던 것은 아닙니다. 부분체결 후 취소된 주문은 statecancel이면서 executed_volume에 체결 수량이 남습니다. "취소니까 버린다"로 짜면 평균단가와 실현손익이 통째로 틀어집니다. 매매 기록을 어디까지 남겨야 하는지는 자동매매 로깅과 매매일지에 정리했습니다.

7. 봇에 넣는 재주문 루틴 (전체 코드)

다섯 가지를 하나로 묶으면 이런 모양입니다. 인증 헤더 생성은 업비트 봇 30분 만들기의 JWT 부분을 그대로 쓴다고 가정합니다.

import requests

BASE = "https://api.upbit.com"

def reprice(prev_uuid, new_price, identifier=None):
    """지정가 주문의 가격만 바꿔 다시 건다. 잔량은 서버가 승계한다."""
    body = {
        "prev_order_uuid": prev_uuid,
        "new_ord_type": "limit",
        "new_volume": "remain_only",   # 잔량 직접 계산 금지
        "new_price": new_price,
    }
    if identifier:
        body["new_identifier"] = identifier   # 원주문 값 재사용 불가

    r = requests.post(BASE + "/v1/orders/cancel_and_new",
                      json=body,                       # data= 아님
                      headers=auth_headers(body),
                      timeout=5)

    if r.status_code == 404:
        return None                    # 이미 체결·취소됨 = 재시도 대상 아님
    if r.status_code == 429:
        raise RateLimited(r.headers.get("Remaining-Req"))
    r.raise_for_status()

    return r.json()["new_order_uuid"]   # uuid가 아니라 new_order_uuid를 추적


def cancel_all_bids(quote="KRW"):
    """미체결 매수만 일괄 취소하고, 남은 예약 주문을 개별로 정리한다."""
    r = requests.delete(BASE + "/v1/orders/open",
                        params={"cancel_side": "bid",
                                "quote_currencies": quote,
                                "count": 300},          # pairs와 동시 사용 불가
                        headers=auth_headers_query(),
                        timeout=5)
    r.raise_for_status()
    result = r.json()

    touched  = [o["uuid"] for o in result["success"]["orders"]]
    touched += [o["uuid"] for o in result["failed"]["orders"]]

    # WATCH(예약) 주문은 위에서 안 지워진다 -> 상태 확인 후 개별 취소
    check = requests.get(BASE + "/v1/orders/uuids",
                         params=[("uuids[]", u) for u in touched[:100]],
                         headers=auth_headers_query(),
                         timeout=5).json()

    for o in check:
        if o["state"] in ("wait", "watch"):
            requests.delete(BASE + "/v1/order",
                            params={"uuid": o["uuid"]},   # body 금지
                            headers=auth_headers_query(),
                            timeout=5)

핵심은 세 줄입니다. 잔량은 remain_only로 넘기고, 추적 대상은 new_order_uuid로 갈아 끼우고, 일괄 취소 뒤에는 반드시 상태를 확인한다.

8. 정리 — 체크리스트

본문의 경로·파라미터·응답 항목명은 2026-08-27 확인 기준 업비트 개발자 문서(docs.upbit.com)에서 확인한 값이고, 응답 예시의 값 자체는 구조 이해용입니다. 거래소 명세와 호출 한도 정책은 예고 없이 바뀝니다 — 업비트도 주문 계열 POST의 요청 형식과 요청 수 제한 기준을 바꾼 이력이 있습니다. 운영에 넣기 전 개발자 문서의 현재 명세로 대조하고, 한도값은 상수로 박지 말고 설정으로 빼 두십시오.

주문 정정·취소까지 제대로 처리하는 봇이 필요하다면

부분체결 잔량 승계, 예약 주문 정리, 호출 한도 그룹별 백오프까지 묶어서 실제로 돌아가는 업비트 자동매매 프로그램으로 만들어 드립니다. 24시간 빠른 답변 가능합니다.

무료 상담 시작하기