From 2e222a46a0ef2d36204fd992cda7f8fa1873b5de Mon Sep 17 00:00:00 2001 From: gylim Date: Sun, 22 Mar 2026 21:51:26 +0900 Subject: [PATCH 1/3] =?UTF-8?q?fix:=20orders=20API=EC=9D=98=20get=5Fchance?= =?UTF-8?q?=20=EB=B0=98=ED=99=98=20=ED=83=80=EC=9E=85=20=EB=B0=8F=20OrderC?= =?UTF-8?q?reated=20=EB=AA=A8=EB=8D=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - get_chance()가 list[OrderChance] 대신 OrderChance를 반환하도록 수정 (Upbit API가 단일 객체를 반환하나 리스트로 처리하고 있었음) - OrderCreated 모델에서 remaining_volume, prevented_volume, prevented_locked을 optional로 변경 (시장가 주문 응답에 해당 필드가 포함되지 않음) - 동기/비동기 양쪽 모두 수정, mock 테스트도 함께 수정 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/upbeat/api/orders.py | 8 ++++---- src/upbeat/types/order.py | 6 +++--- tests/api/test_orders.py | 24 +++++++++++------------- 3 files changed, 18 insertions(+), 20 deletions(-) diff --git a/src/upbeat/api/orders.py b/src/upbeat/api/orders.py index 038924f..a700bfe 100644 --- a/src/upbeat/api/orders.py +++ b/src/upbeat/api/orders.py @@ -235,12 +235,12 @@ def cancel_and_new( ) return CancelAndNewOrderResponse.model_validate(response.data) - def get_chance(self, *, market: str) -> list[OrderChance]: + def get_chance(self, *, market: str) -> OrderChance: params = {"market": market} response = self._transport.request( "GET", "/v1/orders/chance", params=params, credentials=self._credentials ) - return [OrderChance.model_validate(item) for item in response.data] + return OrderChance.model_validate(response.data) class AsyncOrdersAPI(_AsyncAPIResource): @@ -458,9 +458,9 @@ async def cancel_and_new( ) return CancelAndNewOrderResponse.model_validate(response.data) - async def get_chance(self, *, market: str) -> list[OrderChance]: + async def get_chance(self, *, market: str) -> OrderChance: params = {"market": market} response = await self._transport.request( "GET", "/v1/orders/chance", params=params, credentials=self._credentials ) - return [OrderChance.model_validate(item) for item in response.data] + return OrderChance.model_validate(response.data) diff --git a/src/upbeat/types/order.py b/src/upbeat/types/order.py index 81375b8..1484cd4 100644 --- a/src/upbeat/types/order.py +++ b/src/upbeat/types/order.py @@ -32,7 +32,7 @@ class OrderCreated(BaseModel): state: str created_at: str volume: str | None = None - remaining_volume: str + remaining_volume: str | None = None executed_volume: str reserved_fee: str remaining_fee: str @@ -42,8 +42,8 @@ class OrderCreated(BaseModel): time_in_force: str | None = None identifier: str | None = None smp_type: str | None = None - prevented_volume: str - prevented_locked: str + prevented_volume: str | None = None + prevented_locked: str | None = None # ── OrderDetail (GET /v1/order 응답) ──────────────────────────────────── diff --git a/tests/api/test_orders.py b/tests/api/test_orders.py index 96935d9..e0cc632 100644 --- a/tests/api/test_orders.py +++ b/tests/api/test_orders.py @@ -512,27 +512,26 @@ def test_calls_correct_endpoint(self) -> None: def handler(request: httpx.Request) -> httpx.Response: assert request.url.path == "/v1/orders/chance" assert request.url.params["market"] == "KRW-BTC" - return _json_response([ORDER_CHANCE_DATA]) + return _json_response(ORDER_CHANCE_DATA) transport = _make_transport(handler) api = OrdersAPI(transport, CREDENTIALS) result = api.get_chance(market="KRW-BTC") - assert len(result) == 1 - assert isinstance(result[0], OrderChance) - assert result[0].bid_fee == "0.0005" + assert isinstance(result, OrderChance) + assert result.bid_fee == "0.0005" def test_parses_nested_objects(self) -> None: def handler(request: httpx.Request) -> httpx.Response: - return _json_response([ORDER_CHANCE_DATA]) + return _json_response(ORDER_CHANCE_DATA) transport = _make_transport(handler) api = OrdersAPI(transport, CREDENTIALS) result = api.get_chance(market="KRW-BTC") - assert result[0].market.id == "KRW-BTC" - assert result[0].market.bid is not None - assert result[0].market.bid.currency == "KRW" - assert result[0].bid_account.currency == "KRW" - assert result[0].ask_account.currency == "BTC" + assert result.market.id == "KRW-BTC" + assert result.market.bid is not None + assert result.market.bid.currency == "KRW" + assert result.bid_account.currency == "KRW" + assert result.ask_account.currency == "BTC" # ── TestAsyncOrders ────────────────────────────────────────────────────── @@ -578,10 +577,9 @@ async def handler(request: httpx.Request) -> httpx.Response: @pytest.mark.asyncio async def test_get_chance(self) -> None: async def handler(request: httpx.Request) -> httpx.Response: - return _json_response([ORDER_CHANCE_DATA]) + return _json_response(ORDER_CHANCE_DATA) transport = _make_async_transport(handler) api = AsyncOrdersAPI(transport, CREDENTIALS) result = await api.get_chance(market="KRW-BTC") - assert len(result) == 1 - assert isinstance(result[0], OrderChance) + assert isinstance(result, OrderChance) From 557b015e8f78db4e128d1bc8e2f6818640666c41 Mon Sep 17 00:00:00 2001 From: gylim Date: Sun, 22 Mar 2026 21:52:27 +0900 Subject: [PATCH 2/3] test: add VCR cassette-based integration tests for exchange API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Accounts (asset) 및 Orders API에 대한 VCR cassette 통합 테스트 추가. - tests/api/test_accounts_vcr.py: accounts.list() cassette 테스트 - tests/api/test_orders_vcr.py: 안전한 엔드포인트(get_chance, create_test, list_open, list_closed), 지정가 라이프사이클(create→get→cancel, cancel_and_new, list_by_ids, cancel_by_ids, cancel_open), 시장가 매수→매도 테스트 - scripts/record_order_cassettes.py: 시장가 매수→매도 cassette 녹화 스크립트 - tests/_vcr.py: serialize() 시 bytes body를 pretty JSON으로 변환하는 _prettify_response_bodies() 추가 (비압축 응답 대응) Co-Authored-By: Claude Opus 4.6 (1M context) --- scripts/record_order_cassettes.py | 122 +++++++ tests/_vcr.py | 21 ++ tests/api/test_accounts_vcr.py | 34 ++ tests/api/test_orders_vcr.py | 241 +++++++++++++ tests/cassettes/accounts/list.yaml | 52 +++ tests/cassettes/orders/create_test.yaml | 65 ++++ tests/cassettes/orders/get_chance.yaml | 101 ++++++ .../orders/lifecycle_bulk_cancel_by_ids.yaml | 187 ++++++++++ .../orders/lifecycle_cancel_and_new.yaml | 188 ++++++++++ .../orders/lifecycle_cancel_open.yaml | 120 +++++++ .../orders/lifecycle_create_get_cancel.yaml | 184 ++++++++++ .../orders/lifecycle_list_by_ids.yaml | 186 ++++++++++ tests/cassettes/orders/list_closed.yaml | 324 ++++++++++++++++++ tests/cassettes/orders/list_open.yaml | 42 +++ tests/cassettes/orders/market_buy_sell.yaml | 262 ++++++++++++++ 15 files changed, 2129 insertions(+) create mode 100644 scripts/record_order_cassettes.py create mode 100644 tests/api/test_accounts_vcr.py create mode 100644 tests/api/test_orders_vcr.py create mode 100644 tests/cassettes/accounts/list.yaml create mode 100644 tests/cassettes/orders/create_test.yaml create mode 100644 tests/cassettes/orders/get_chance.yaml create mode 100644 tests/cassettes/orders/lifecycle_bulk_cancel_by_ids.yaml create mode 100644 tests/cassettes/orders/lifecycle_cancel_and_new.yaml create mode 100644 tests/cassettes/orders/lifecycle_cancel_open.yaml create mode 100644 tests/cassettes/orders/lifecycle_create_get_cancel.yaml create mode 100644 tests/cassettes/orders/lifecycle_list_by_ids.yaml create mode 100644 tests/cassettes/orders/list_closed.yaml create mode 100644 tests/cassettes/orders/list_open.yaml create mode 100644 tests/cassettes/orders/market_buy_sell.yaml diff --git a/scripts/record_order_cassettes.py b/scripts/record_order_cassettes.py new file mode 100644 index 0000000..9cf48b4 --- /dev/null +++ b/scripts/record_order_cassettes.py @@ -0,0 +1,122 @@ +"""시장가 매수→매도 VCR cassette 녹화 스크립트. + +실제 거래가 발생하므로 소량의 금전적 손실(스프레드+수수료)이 발생한다. +KRW-BTC 마켓에서 최소금액(5,000원)으로 매수 후 즉시 전량 매도한다. + +사용법: + UPBIT_ACCESS_KEY=xxx UPBIT_SECRET_KEY=yyy \ + uv run python scripts/record_order_cassettes.py +""" + +from __future__ import annotations + +import os +import sys +import time + +# 프로젝트 루트의 tests 모듈을 import하기 위해 경로 추가 +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +from tests._vcr import upbeat_vcr # noqa: E402 + +from upbeat import Upbeat # noqa: E402 + +_MARKET = "KRW-BTC" +_BUY_AMOUNT = "10000" # 매도 시 최소금액(5000원) 확보를 위해 넉넉히 +_CASSETTE_PATH = "orders/market_buy_sell.yaml" + + +def _require_env(name: str) -> str: + value = os.environ.get(name) + if not value: + print(f"ERROR: {name} 환경변수가 설정되지 않았습니다.") + sys.exit(1) + return value + + +def main() -> None: + access_key = _require_env("UPBIT_ACCESS_KEY") + secret_key = _require_env("UPBIT_SECRET_KEY") + + print("=" * 60) + print("시장가 매수→매도 VCR Cassette 녹화") + print("=" * 60) + print(f" 마켓: {_MARKET}") + print(f" 매수 금액: {_BUY_AMOUNT}원") + print(" 예상 손실: 스프레드 + 수수료 ≈ 50~100원") + print(f" Cassette: {_CASSETTE_PATH}") + print("=" * 60) + print() + + confirm = input("계속하시겠습니까? (y/N): ").strip().lower() + if confirm != "y": + print("취소되었습니다.") + sys.exit(0) + + client = Upbeat( + access_key=access_key, + secret_key=secret_key, + max_retries=0, + auto_throttle=False, + ) + + with client, upbeat_vcr.use_cassette( + _CASSETTE_PATH, record_mode="all" + ): + # 1) 시장가 매수 + print(f"\n[1/4] 시장가 매수 ({_MARKET}, {_BUY_AMOUNT}원)...") + buy = client.orders.create( + market=_MARKET, + side="bid", + ord_type="price", + price=_BUY_AMOUNT, + ) + print(f" 주문 생성: uuid={buy.uuid}, state={buy.state}") + + # 체결 대기 + time.sleep(1) + + # 2) 매수 주문 상태 조회 + print("[2/4] 매수 주문 상태 조회...") + buy_detail = client.orders.get(uuid=buy.uuid) + print( + f" state={buy_detail.state}, " + f"executed_volume={buy_detail.executed_volume}" + ) + + if buy_detail.executed_volume == "0": + print("ERROR: 매수 체결량이 0입니다. 매도를 건너뜁니다.") + sys.exit(1) + + # 3) 매수 수량 전량 시장가 매도 + print(f"[3/4] 시장가 매도 (volume={buy_detail.executed_volume})...") + try: + sell = client.orders.create( + market=_MARKET, + side="ask", + ord_type="market", + volume=buy_detail.executed_volume, + ) + print(f" 주문 생성: uuid={sell.uuid}, state={sell.state}") + except Exception as e: + print(f"ERROR: 매도 실패 — {e}") + print(" 수동으로 매도해야 합니다!") + sys.exit(1) + + # 체결 대기 + time.sleep(1) + + # 4) 매도 주문 상태 조회 + print("[4/4] 매도 주문 상태 조회...") + sell_detail = client.orders.get(uuid=sell.uuid) + print( + f" state={sell_detail.state}, " + f"executed_volume={sell_detail.executed_volume}" + ) + + print(f"\nCassette 저장 완료: tests/cassettes/{_CASSETTE_PATH}") + print("녹화가 완료되었습니다.") + + +if __name__ == "__main__": + main() diff --git a/tests/_vcr.py b/tests/_vcr.py index 9d0c9b5..d9a205c 100644 --- a/tests/_vcr.py +++ b/tests/_vcr.py @@ -75,7 +75,28 @@ def _ensure_body_bytes(cassette_dict: dict) -> dict: return cassette_dict +def _prettify_response_bodies(cassette_dict: dict) -> None: + """serialize 직전에 bytes body를 pretty JSON 문자열로 변환한다.""" + for interaction in cassette_dict.get("interactions", []): + body = interaction.get("response", {}).get("body") + if not isinstance(body, dict): + continue + raw = body.get("string", "") + if isinstance(raw, bytes): + raw = raw.decode("utf-8") + try: + parsed = json.loads(raw) + body["string"] = json.dumps( + parsed, indent=2, ensure_ascii=False, sort_keys=False + ) + except (json.JSONDecodeError, TypeError): + # JSON이 아니어도 bytes → str 변환은 필요 (YAML 저장용) + if isinstance(body.get("string"), bytes): + body["string"] = body["string"].decode("utf-8") + + def serialize(cassette_dict: dict) -> str: + _prettify_response_bodies(cassette_dict) return yaml.dump( _mark_multiline(cassette_dict), default_flow_style=False, diff --git a/tests/api/test_accounts_vcr.py b/tests/api/test_accounts_vcr.py new file mode 100644 index 0000000..56534fc --- /dev/null +++ b/tests/api/test_accounts_vcr.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +import os + +from tests.conftest import upbeat_vcr +from upbeat import Upbeat +from upbeat.types.account import Account + + +def _client() -> Upbeat: + return Upbeat( + access_key=os.environ.get("UPBIT_ACCESS_KEY", "test-key"), + secret_key=os.environ.get("UPBIT_SECRET_KEY", "test-secret"), + max_retries=0, + auto_throttle=False, + ) + + +# ── Accounts ─────────────────────────────────────────────────────────── + + +class TestListAccounts: + @upbeat_vcr.use_cassette("accounts/list.yaml") + def test_returns_valid_accounts(self) -> None: + with _client() as client: + result = client.accounts.list() + + assert len(result) >= 1 + account = result[0] + assert isinstance(account, Account) + assert isinstance(account.currency, str) + assert isinstance(account.balance, str) + assert isinstance(account.locked, str) + assert isinstance(account.unit_currency, str) diff --git a/tests/api/test_orders_vcr.py b/tests/api/test_orders_vcr.py new file mode 100644 index 0000000..4c81a36 --- /dev/null +++ b/tests/api/test_orders_vcr.py @@ -0,0 +1,241 @@ +from __future__ import annotations + +import os + +from tests.conftest import upbeat_vcr +from upbeat import Upbeat +from upbeat.types.order import ( + CancelAndNewOrderResponse, + CancelResult, + OrderByIds, + OrderCanceled, + OrderChance, + OrderCreated, + OrderDetail, + OrderOpen, +) + +_MARKET = "KRW-BTC" +# 절대 체결되지 않는 극저가 지정가 (BTC ~1.4억원 대비 1천만원) +_LIMIT_PRICE = "10000000" +_LIMIT_VOLUME = "0.0005" # total = 5,000 KRW ≥ 최소주문금액 + + +def _client() -> Upbeat: + return Upbeat( + access_key=os.environ.get("UPBIT_ACCESS_KEY", "test-key"), + secret_key=os.environ.get("UPBIT_SECRET_KEY", "test-secret"), + max_retries=0, + auto_throttle=False, + ) + + +# ── Safe endpoints (거래 불필요) ─────────────────────────────────────── + + +class TestGetChance: + @upbeat_vcr.use_cassette("orders/get_chance.yaml") + def test_returns_valid_chance(self) -> None: + with _client() as client: + result = client.orders.get_chance(market=_MARKET) + + assert isinstance(result, OrderChance) + assert isinstance(result.bid_fee, str) + assert isinstance(result.ask_fee, str) + assert result.market.id == _MARKET + + +class TestCreateTest: + @upbeat_vcr.use_cassette("orders/create_test.yaml") + def test_creates_test_order(self) -> None: + with _client() as client: + result = client.orders.create_test( + market=_MARKET, + side="bid", + ord_type="limit", + price=_LIMIT_PRICE, + volume=_LIMIT_VOLUME, + ) + + assert isinstance(result, OrderCreated) + assert result.market == _MARKET + assert result.side == "bid" + + +class TestListOpen: + @upbeat_vcr.use_cassette("orders/list_open.yaml") + def test_returns_open_orders(self) -> None: + with _client() as client: + result = client.orders.list_open() + + assert isinstance(result, list) + for order in result: + assert isinstance(order, OrderOpen) + + +class TestListClosed: + @upbeat_vcr.use_cassette("orders/list_closed.yaml") + def test_returns_closed_orders(self) -> None: + with _client() as client: + result = client.orders.list_closed() + + assert isinstance(result, list) + + +# ── Lifecycle: create → get → cancel (비용 0원) ─────────────────────── + + +class TestOrderLifecycle: + @upbeat_vcr.use_cassette("orders/lifecycle_create_get_cancel.yaml") + def test_create_get_and_cancel(self) -> None: + with _client() as client: + created = client.orders.create( + market=_MARKET, + side="bid", + ord_type="limit", + price=_LIMIT_PRICE, + volume=_LIMIT_VOLUME, + ) + assert isinstance(created, OrderCreated) + assert created.side == "bid" + assert created.state == "wait" + + detail = client.orders.get(uuid=created.uuid) + assert isinstance(detail, OrderDetail) + assert detail.uuid == created.uuid + assert detail.market == _MARKET + + canceled = client.orders.cancel(uuid=created.uuid) + assert isinstance(canceled, OrderCanceled) + assert canceled.uuid == created.uuid + + +# ── Lifecycle: cancel_and_new ───────────────────────────────────────── + + +class TestCancelAndNew: + @upbeat_vcr.use_cassette("orders/lifecycle_cancel_and_new.yaml") + def test_cancel_and_replace(self) -> None: + with _client() as client: + created = client.orders.create( + market=_MARKET, + side="bid", + ord_type="limit", + price=_LIMIT_PRICE, + volume=_LIMIT_VOLUME, + ) + + replaced = client.orders.cancel_and_new( + prev_order_uuid=created.uuid, + new_ord_type="limit", + new_price="10500000", + new_volume=_LIMIT_VOLUME, + ) + assert isinstance(replaced, CancelAndNewOrderResponse) + assert replaced.new_order_uuid + + client.orders.cancel(uuid=replaced.new_order_uuid) + + +# ── Lifecycle: list_by_ids ──────────────────────────────────────────── + + +class TestListByIds: + @upbeat_vcr.use_cassette("orders/lifecycle_list_by_ids.yaml") + def test_list_by_ids(self) -> None: + with _client() as client: + created = client.orders.create( + market=_MARKET, + side="bid", + ord_type="limit", + price=_LIMIT_PRICE, + volume=_LIMIT_VOLUME, + ) + + result = client.orders.list_by_ids(uuids=[created.uuid]) + assert len(result) >= 1 + assert isinstance(result[0], OrderByIds) + assert result[0].uuid == created.uuid + + client.orders.cancel(uuid=created.uuid) + + +# ── Lifecycle: bulk cancel_by_ids ───────────────────────────────────── + + +class TestBulkCancelByIds: + @upbeat_vcr.use_cassette("orders/lifecycle_bulk_cancel_by_ids.yaml") + def test_cancel_by_ids(self) -> None: + with _client() as client: + o1 = client.orders.create( + market=_MARKET, + side="bid", + ord_type="limit", + price=_LIMIT_PRICE, + volume=_LIMIT_VOLUME, + ) + o2 = client.orders.create( + market=_MARKET, + side="bid", + ord_type="limit", + price="10100000", + volume=_LIMIT_VOLUME, + ) + + result = client.orders.cancel_by_ids(uuids=[o1.uuid, o2.uuid]) + assert isinstance(result, CancelResult) + assert result.success.count == 2 + + +# ── Lifecycle: cancel_open (마지막에 실행) ──────────────────────────── + + +class TestCancelOpen: + @upbeat_vcr.use_cassette("orders/lifecycle_cancel_open.yaml") + def test_cancel_open(self) -> None: + with _client() as client: + client.orders.create( + market=_MARKET, + side="bid", + ord_type="limit", + price=_LIMIT_PRICE, + volume=_LIMIT_VOLUME, + ) + + result = client.orders.cancel_open() + assert isinstance(result, CancelResult) + assert result.success.count >= 1 + + +# ── Market buy → sell (시장가 체결 테스트) ───────────────────────────── + + +class TestMarketBuySell: + @upbeat_vcr.use_cassette("orders/market_buy_sell.yaml") + def test_market_buy_and_sell(self) -> None: + with _client() as client: + # 시장가 매수 (매도 최소금액 확보를 위해 10,000원) + buy = client.orders.create( + market=_MARKET, + side="bid", + ord_type="price", + price="10000", + ) + assert isinstance(buy, OrderCreated) + assert buy.side == "bid" + + buy_detail = client.orders.get(uuid=buy.uuid) + assert isinstance(buy_detail, OrderDetail) + + # 매수한 수량 전량 시장가 매도 + sell = client.orders.create( + market=_MARKET, + side="ask", + ord_type="market", + volume=buy_detail.executed_volume, + ) + assert isinstance(sell, OrderCreated) + assert sell.side == "ask" + + sell_detail = client.orders.get(uuid=sell.uuid) + assert isinstance(sell_detail, OrderDetail) diff --git a/tests/cassettes/accounts/list.yaml b/tests/cassettes/accounts/list.yaml new file mode 100644 index 0000000..f2efbd8 --- /dev/null +++ b/tests/cassettes/accounts/list.yaml @@ -0,0 +1,52 @@ +interactions: +- request: + body: '' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - REDACTED + Connection: + - keep-alive + Host: + - api.upbit.com + User-Agent: + - python-httpx/0.28.1 + method: GET + uri: https://api.upbit.com/v1/accounts + response: + body: + string: |- + [ + { + "currency": "KRW", + "balance": "50000.47088291", + "locked": "0", + "avg_buy_price": "0", + "avg_buy_price_modified": true, + "unit_currency": "KRW" + } + ] + headers: + Cache-Control: + - no-cache, no-store, max-age=0, must-revalidate + Connection: + - keep-alive + Content-Type: + - application/json;charset=UTF-8 + Date: + - Sun, 22 Mar 2026 12:43:17 GMT + Remaining-Req: + - group=default; min=1800; sec=29 + Transfer-Encoding: + - chunked + Vary: + - Origin + - Access-Control-Request-Method + - Access-Control-Request-Headers + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/orders/create_test.yaml b/tests/cassettes/orders/create_test.yaml new file mode 100644 index 0000000..f045162 --- /dev/null +++ b/tests/cassettes/orders/create_test.yaml @@ -0,0 +1,65 @@ +interactions: +- request: + body: '{"market":"KRW-BTC","side":"bid","volume":"0.0005","price":"10000000","ord_type":"limit"}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - REDACTED + Connection: + - keep-alive + Content-Length: + - '89' + Content-Type: + - application/json; charset=utf-8 + Host: + - api.upbit.com + User-Agent: + - python-httpx/0.28.1 + method: POST + uri: https://api.upbit.com/v1/orders/test + response: + body: + string: |- + { + "uuid": "001f3413-6425-45b6-89dd-0b8883fc5c59", + "side": "bid", + "ord_type": "limit", + "price": "10000000", + "state": "wait", + "market": "KRW-BTC", + "created_at": "2026-03-22T21:42:16+09:00", + "volume": "0.0005", + "remaining_volume": "0.0005", + "prevented_volume": "0", + "reserved_fee": "2.5", + "remaining_fee": "2.5", + "paid_fee": "0", + "locked": "5002.5", + "prevented_locked": "0", + "executed_volume": "0", + "trades_count": 0 + } + headers: + Cache-Control: + - no-cache, no-store, max-age=0, must-revalidate + Connection: + - keep-alive + Content-Type: + - application/json;charset=UTF-8 + Date: + - Sun, 22 Mar 2026 12:42:16 GMT + Remaining-Req: + - group=order-test; min=480; sec=7 + Transfer-Encoding: + - chunked + Vary: + - Origin + - Access-Control-Request-Method + - Access-Control-Request-Headers + status: + code: 201 + message: Created +version: 1 diff --git a/tests/cassettes/orders/get_chance.yaml b/tests/cassettes/orders/get_chance.yaml new file mode 100644 index 0000000..2a9a979 --- /dev/null +++ b/tests/cassettes/orders/get_chance.yaml @@ -0,0 +1,101 @@ +interactions: +- request: + body: '' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - REDACTED + Connection: + - keep-alive + Host: + - api.upbit.com + User-Agent: + - python-httpx/0.28.1 + method: GET + uri: https://api.upbit.com/v1/orders/chance?market=KRW-BTC + response: + body: + string: |- + { + "bid_fee": "0.0005", + "ask_fee": "0.0005", + "maker_bid_fee": "0.0005", + "maker_ask_fee": "0.0005", + "market": { + "id": "KRW-BTC", + "name": "BTC/KRW", + "order_types": [ + "limit" + ], + "order_sides": [ + "ask", + "bid" + ], + "bid_types": [ + "best_fok", + "best_ioc", + "limit", + "limit_fok", + "limit_ioc", + "price" + ], + "ask_types": [ + "best_fok", + "best_ioc", + "limit", + "limit_fok", + "limit_ioc", + "market" + ], + "bid": { + "currency": "KRW", + "min_total": "5000" + }, + "ask": { + "currency": "BTC", + "min_total": "5000" + }, + "max_total": "1000000000", + "state": "active" + }, + "bid_account": { + "currency": "KRW", + "balance": "50000.47088291", + "locked": "0", + "avg_buy_price": "0", + "avg_buy_price_modified": true, + "unit_currency": "KRW" + }, + "ask_account": { + "currency": "BTC", + "balance": "0", + "locked": "0", + "avg_buy_price": "0", + "avg_buy_price_modified": false, + "unit_currency": "KRW" + } + } + headers: + Cache-Control: + - no-cache, no-store, max-age=0, must-revalidate + Connection: + - keep-alive + Content-Type: + - application/json;charset=UTF-8 + Date: + - Sun, 22 Mar 2026 12:42:12 GMT + Remaining-Req: + - group=default; min=1800; sec=29 + Transfer-Encoding: + - chunked + Vary: + - Origin + - Access-Control-Request-Method + - Access-Control-Request-Headers + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/orders/lifecycle_bulk_cancel_by_ids.yaml b/tests/cassettes/orders/lifecycle_bulk_cancel_by_ids.yaml new file mode 100644 index 0000000..371a027 --- /dev/null +++ b/tests/cassettes/orders/lifecycle_bulk_cancel_by_ids.yaml @@ -0,0 +1,187 @@ +interactions: +- request: + body: '{"market":"KRW-BTC","side":"bid","volume":"0.0005","price":"10000000","ord_type":"limit"}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - REDACTED + Connection: + - keep-alive + Content-Length: + - '89' + Content-Type: + - application/json; charset=utf-8 + Host: + - api.upbit.com + User-Agent: + - python-httpx/0.28.1 + method: POST + uri: https://api.upbit.com/v1/orders + response: + body: + string: |- + { + "uuid": "2adc81d8-5497-403f-826e-3ce93af457cf", + "side": "bid", + "ord_type": "limit", + "price": "10000000", + "state": "wait", + "market": "KRW-BTC", + "created_at": "2026-03-22T21:42:28+09:00", + "volume": "0.0005", + "remaining_volume": "0.0005", + "prevented_volume": "0", + "reserved_fee": "2.5", + "remaining_fee": "2.5", + "paid_fee": "0", + "locked": "5002.5", + "prevented_locked": "0", + "executed_volume": "0", + "trades_count": 0 + } + headers: + Cache-Control: + - no-cache, no-store, max-age=0, must-revalidate + Connection: + - keep-alive + Content-Type: + - application/json;charset=UTF-8 + Date: + - Sun, 22 Mar 2026 12:42:28 GMT + Remaining-Req: + - group=order; min=480; sec=6 + Transfer-Encoding: + - chunked + Vary: + - Origin + - Access-Control-Request-Method + - Access-Control-Request-Headers + status: + code: 201 + message: Created +- request: + body: '{"market":"KRW-BTC","side":"bid","volume":"0.0005","price":"10100000","ord_type":"limit"}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - REDACTED + Connection: + - keep-alive + Content-Length: + - '89' + Content-Type: + - application/json; charset=utf-8 + Host: + - api.upbit.com + User-Agent: + - python-httpx/0.28.1 + method: POST + uri: https://api.upbit.com/v1/orders + response: + body: + string: |- + { + "uuid": "c8f748a1-4b03-428e-8b58-1eec1d87acc9", + "side": "bid", + "ord_type": "limit", + "price": "10100000", + "state": "wait", + "market": "KRW-BTC", + "created_at": "2026-03-22T21:42:28+09:00", + "volume": "0.0005", + "remaining_volume": "0.0005", + "prevented_volume": "0", + "reserved_fee": "2.525", + "remaining_fee": "2.525", + "paid_fee": "0", + "locked": "5052.525", + "prevented_locked": "0", + "executed_volume": "0", + "trades_count": 0 + } + headers: + Cache-Control: + - no-cache, no-store, max-age=0, must-revalidate + Connection: + - keep-alive + Content-Type: + - application/json;charset=UTF-8 + Date: + - Sun, 22 Mar 2026 12:42:28 GMT + Remaining-Req: + - group=order; min=480; sec=5 + Transfer-Encoding: + - chunked + Vary: + - Origin + - Access-Control-Request-Method + - Access-Control-Request-Headers + status: + code: 201 + message: Created +- request: + body: '' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - REDACTED + Connection: + - keep-alive + Host: + - api.upbit.com + User-Agent: + - python-httpx/0.28.1 + method: DELETE + uri: https://api.upbit.com/v1/orders/uuids?uuids%5B%5D=2adc81d8-5497-403f-826e-3ce93af457cf&uuids%5B%5D=c8f748a1-4b03-428e-8b58-1eec1d87acc9 + response: + body: + string: |- + { + "success": { + "count": 2, + "orders": [ + { + "uuid": "c8f748a1-4b03-428e-8b58-1eec1d87acc9", + "market": "KRW-BTC" + }, + { + "uuid": "2adc81d8-5497-403f-826e-3ce93af457cf", + "market": "KRW-BTC" + } + ] + }, + "failed": { + "count": 0, + "orders": [] + } + } + headers: + Cache-Control: + - no-cache, no-store, max-age=0, must-revalidate + Connection: + - keep-alive + Content-Type: + - application/json;charset=UTF-8 + Date: + - Sun, 22 Mar 2026 12:42:28 GMT + Remaining-Req: + - group=default; min=1800; sec=28 + Transfer-Encoding: + - chunked + Vary: + - Origin + - Access-Control-Request-Method + - Access-Control-Request-Headers + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/orders/lifecycle_cancel_and_new.yaml b/tests/cassettes/orders/lifecycle_cancel_and_new.yaml new file mode 100644 index 0000000..ebb3d49 --- /dev/null +++ b/tests/cassettes/orders/lifecycle_cancel_and_new.yaml @@ -0,0 +1,188 @@ +interactions: +- request: + body: '{"market":"KRW-BTC","side":"bid","volume":"0.0005","price":"10000000","ord_type":"limit"}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - REDACTED + Connection: + - keep-alive + Content-Length: + - '89' + Content-Type: + - application/json; charset=utf-8 + Host: + - api.upbit.com + User-Agent: + - python-httpx/0.28.1 + method: POST + uri: https://api.upbit.com/v1/orders + response: + body: + string: |- + { + "uuid": "348ad871-1f2e-4caa-bb9d-b6b4ae4d333e", + "side": "bid", + "ord_type": "limit", + "price": "10000000", + "state": "wait", + "market": "KRW-BTC", + "created_at": "2026-03-22T21:42:26+09:00", + "volume": "0.0005", + "remaining_volume": "0.0005", + "prevented_volume": "0", + "reserved_fee": "2.5", + "remaining_fee": "2.5", + "paid_fee": "0", + "locked": "5002.5", + "prevented_locked": "0", + "executed_volume": "0", + "trades_count": 0 + } + headers: + Cache-Control: + - no-cache, no-store, max-age=0, must-revalidate + Connection: + - keep-alive + Content-Type: + - application/json;charset=UTF-8 + Date: + - Sun, 22 Mar 2026 12:42:26 GMT + Remaining-Req: + - group=order; min=480; sec=7 + Transfer-Encoding: + - chunked + Vary: + - Origin + - Access-Control-Request-Method + - Access-Control-Request-Headers + status: + code: 201 + message: Created +- request: + body: '{"new_ord_type":"limit","prev_order_uuid":"348ad871-1f2e-4caa-bb9d-b6b4ae4d333e","new_volume":"0.0005","new_price":"10500000"}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - REDACTED + Connection: + - keep-alive + Content-Length: + - '126' + Content-Type: + - application/json; charset=utf-8 + Host: + - api.upbit.com + User-Agent: + - python-httpx/0.28.1 + method: POST + uri: https://api.upbit.com/v1/orders/cancel_and_new + response: + body: + string: |- + { + "uuid": "348ad871-1f2e-4caa-bb9d-b6b4ae4d333e", + "side": "bid", + "ord_type": "limit", + "price": "10000000", + "state": "wait", + "market": "KRW-BTC", + "created_at": "2026-03-22T21:42:26+09:00", + "volume": "0.0005", + "remaining_volume": "0.0005", + "prevented_volume": "0", + "reserved_fee": "2.5", + "remaining_fee": "2.5", + "paid_fee": "0", + "locked": "5002.5", + "prevented_locked": "0", + "executed_volume": "0", + "trades_count": 0, + "new_order_uuid": "5cb0988d-879d-405d-93e0-2530e9d6dff2" + } + headers: + Cache-Control: + - no-cache, no-store, max-age=0, must-revalidate + Connection: + - keep-alive + Content-Type: + - application/json;charset=UTF-8 + Date: + - Sun, 22 Mar 2026 12:42:26 GMT + Remaining-Req: + - group=order; min=480; sec=6 + Transfer-Encoding: + - chunked + Vary: + - Origin + - Access-Control-Request-Method + - Access-Control-Request-Headers + status: + code: 200 + message: OK +- request: + body: '' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - REDACTED + Connection: + - keep-alive + Host: + - api.upbit.com + User-Agent: + - python-httpx/0.28.1 + method: DELETE + uri: https://api.upbit.com/v1/order?uuid=5cb0988d-879d-405d-93e0-2530e9d6dff2 + response: + body: + string: |- + { + "uuid": "5cb0988d-879d-405d-93e0-2530e9d6dff2", + "side": "bid", + "ord_type": "limit", + "price": "10500000", + "state": "wait", + "market": "KRW-BTC", + "created_at": "2026-03-22T21:42:27+09:00", + "volume": "0.0005", + "remaining_volume": "0.0005", + "prevented_volume": "0", + "reserved_fee": "2.625", + "remaining_fee": "2.625", + "paid_fee": "0", + "locked": "5252.625", + "prevented_locked": "0", + "executed_volume": "0", + "trades_count": 0 + } + headers: + Cache-Control: + - no-cache, no-store, max-age=0, must-revalidate + Connection: + - keep-alive + Content-Type: + - application/json;charset=UTF-8 + Date: + - Sun, 22 Mar 2026 12:42:27 GMT + Remaining-Req: + - group=default; min=1800; sec=28 + Transfer-Encoding: + - chunked + Vary: + - Origin + - Access-Control-Request-Method + - Access-Control-Request-Headers + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/orders/lifecycle_cancel_open.yaml b/tests/cassettes/orders/lifecycle_cancel_open.yaml new file mode 100644 index 0000000..944829f --- /dev/null +++ b/tests/cassettes/orders/lifecycle_cancel_open.yaml @@ -0,0 +1,120 @@ +interactions: +- request: + body: '{"market":"KRW-BTC","side":"bid","volume":"0.0005","price":"10000000","ord_type":"limit"}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - REDACTED + Connection: + - keep-alive + Content-Length: + - '89' + Content-Type: + - application/json; charset=utf-8 + Host: + - api.upbit.com + User-Agent: + - python-httpx/0.28.1 + method: POST + uri: https://api.upbit.com/v1/orders + response: + body: + string: |- + { + "uuid": "ae6a8b3c-0d98-4bf9-a9fa-3d7ebfc67c90", + "side": "bid", + "ord_type": "limit", + "price": "10000000", + "state": "wait", + "market": "KRW-BTC", + "created_at": "2026-03-22T21:42:29+09:00", + "volume": "0.0005", + "remaining_volume": "0.0005", + "prevented_volume": "0", + "reserved_fee": "2.5", + "remaining_fee": "2.5", + "paid_fee": "0", + "locked": "5002.5", + "prevented_locked": "0", + "executed_volume": "0", + "trades_count": 0 + } + headers: + Cache-Control: + - no-cache, no-store, max-age=0, must-revalidate + Connection: + - keep-alive + Content-Type: + - application/json;charset=UTF-8 + Date: + - Sun, 22 Mar 2026 12:42:29 GMT + Remaining-Req: + - group=order; min=480; sec=5 + Transfer-Encoding: + - chunked + Vary: + - Origin + - Access-Control-Request-Method + - Access-Control-Request-Headers + status: + code: 201 + message: Created +- request: + body: '' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - REDACTED + Connection: + - keep-alive + Host: + - api.upbit.com + User-Agent: + - python-httpx/0.28.1 + method: DELETE + uri: https://api.upbit.com/v1/orders/open + response: + body: + string: |- + { + "success": { + "count": 1, + "orders": [ + { + "uuid": "ae6a8b3c-0d98-4bf9-a9fa-3d7ebfc67c90", + "market": "KRW-BTC" + } + ] + }, + "failed": { + "count": 0, + "orders": [] + } + } + headers: + Cache-Control: + - no-cache, no-store, max-age=0, must-revalidate + Connection: + - keep-alive + Content-Type: + - application/json;charset=UTF-8 + Date: + - Sun, 22 Mar 2026 12:42:29 GMT + Remaining-Req: + - group=order-cancel-all; min=30; sec=0 + Transfer-Encoding: + - chunked + Vary: + - Origin + - Access-Control-Request-Method + - Access-Control-Request-Headers + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/orders/lifecycle_create_get_cancel.yaml b/tests/cassettes/orders/lifecycle_create_get_cancel.yaml new file mode 100644 index 0000000..2322a65 --- /dev/null +++ b/tests/cassettes/orders/lifecycle_create_get_cancel.yaml @@ -0,0 +1,184 @@ +interactions: +- request: + body: '{"market":"KRW-BTC","side":"bid","volume":"0.0005","price":"10000000","ord_type":"limit"}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - REDACTED + Connection: + - keep-alive + Content-Length: + - '89' + Content-Type: + - application/json; charset=utf-8 + Host: + - api.upbit.com + User-Agent: + - python-httpx/0.28.1 + method: POST + uri: https://api.upbit.com/v1/orders + response: + body: + string: |- + { + "uuid": "82fe9b72-eb0f-462b-8e0e-e74ad844acea", + "side": "bid", + "ord_type": "limit", + "price": "10000000", + "state": "wait", + "market": "KRW-BTC", + "created_at": "2026-03-22T21:42:22+09:00", + "volume": "0.0005", + "remaining_volume": "0.0005", + "prevented_volume": "0", + "reserved_fee": "2.5", + "remaining_fee": "2.5", + "paid_fee": "0", + "locked": "5002.5", + "prevented_locked": "0", + "executed_volume": "0", + "trades_count": 0 + } + headers: + Cache-Control: + - no-cache, no-store, max-age=0, must-revalidate + Connection: + - keep-alive + Content-Type: + - application/json;charset=UTF-8 + Date: + - Sun, 22 Mar 2026 12:42:22 GMT + Remaining-Req: + - group=order; min=480; sec=7 + Transfer-Encoding: + - chunked + Vary: + - Origin + - Access-Control-Request-Method + - Access-Control-Request-Headers + status: + code: 201 + message: Created +- request: + body: '' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - REDACTED + Connection: + - keep-alive + Host: + - api.upbit.com + User-Agent: + - python-httpx/0.28.1 + method: GET + uri: https://api.upbit.com/v1/order?uuid=82fe9b72-eb0f-462b-8e0e-e74ad844acea + response: + body: + string: |- + { + "uuid": "82fe9b72-eb0f-462b-8e0e-e74ad844acea", + "side": "bid", + "ord_type": "limit", + "price": "10000000", + "state": "wait", + "market": "KRW-BTC", + "created_at": "2026-03-22T21:42:22+09:00", + "volume": "0.0005", + "remaining_volume": "0.0005", + "prevented_volume": "0", + "reserved_fee": "2.5", + "remaining_fee": "2.5", + "paid_fee": "0", + "locked": "5002.5", + "prevented_locked": "0", + "executed_volume": "0", + "trades_count": 0, + "trades": [] + } + headers: + Cache-Control: + - no-cache, no-store, max-age=0, must-revalidate + Connection: + - keep-alive + Content-Type: + - application/json;charset=UTF-8 + Date: + - Sun, 22 Mar 2026 12:42:23 GMT + Remaining-Req: + - group=default; min=1800; sec=29 + Transfer-Encoding: + - chunked + Vary: + - Origin + - Access-Control-Request-Method + - Access-Control-Request-Headers + status: + code: 200 + message: OK +- request: + body: '' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - REDACTED + Connection: + - keep-alive + Host: + - api.upbit.com + User-Agent: + - python-httpx/0.28.1 + method: DELETE + uri: https://api.upbit.com/v1/order?uuid=82fe9b72-eb0f-462b-8e0e-e74ad844acea + response: + body: + string: |- + { + "uuid": "82fe9b72-eb0f-462b-8e0e-e74ad844acea", + "side": "bid", + "ord_type": "limit", + "price": "10000000", + "state": "wait", + "market": "KRW-BTC", + "created_at": "2026-03-22T21:42:22+09:00", + "volume": "0.0005", + "remaining_volume": "0.0005", + "prevented_volume": "0", + "reserved_fee": "2.5", + "remaining_fee": "2.5", + "paid_fee": "0", + "locked": "5002.5", + "prevented_locked": "0", + "executed_volume": "0", + "trades_count": 0 + } + headers: + Cache-Control: + - no-cache, no-store, max-age=0, must-revalidate + Connection: + - keep-alive + Content-Type: + - application/json;charset=UTF-8 + Date: + - Sun, 22 Mar 2026 12:42:26 GMT + Remaining-Req: + - group=default; min=1800; sec=29 + Transfer-Encoding: + - chunked + Vary: + - Origin + - Access-Control-Request-Method + - Access-Control-Request-Headers + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/orders/lifecycle_list_by_ids.yaml b/tests/cassettes/orders/lifecycle_list_by_ids.yaml new file mode 100644 index 0000000..3d5bc18 --- /dev/null +++ b/tests/cassettes/orders/lifecycle_list_by_ids.yaml @@ -0,0 +1,186 @@ +interactions: +- request: + body: '{"market":"KRW-BTC","side":"bid","volume":"0.0005","price":"10000000","ord_type":"limit"}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - REDACTED + Connection: + - keep-alive + Content-Length: + - '89' + Content-Type: + - application/json; charset=utf-8 + Host: + - api.upbit.com + User-Agent: + - python-httpx/0.28.1 + method: POST + uri: https://api.upbit.com/v1/orders + response: + body: + string: |- + { + "uuid": "4f1d13a9-dc4d-4621-b1fe-7cb94f425006", + "side": "bid", + "ord_type": "limit", + "price": "10000000", + "state": "wait", + "market": "KRW-BTC", + "created_at": "2026-03-22T21:42:27+09:00", + "volume": "0.0005", + "remaining_volume": "0.0005", + "prevented_volume": "0", + "reserved_fee": "2.5", + "remaining_fee": "2.5", + "paid_fee": "0", + "locked": "5002.5", + "prevented_locked": "0", + "executed_volume": "0", + "trades_count": 0 + } + headers: + Cache-Control: + - no-cache, no-store, max-age=0, must-revalidate + Connection: + - keep-alive + Content-Type: + - application/json;charset=UTF-8 + Date: + - Sun, 22 Mar 2026 12:42:27 GMT + Remaining-Req: + - group=order; min=480; sec=5 + Transfer-Encoding: + - chunked + Vary: + - Origin + - Access-Control-Request-Method + - Access-Control-Request-Headers + status: + code: 201 + message: Created +- request: + body: '' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - REDACTED + Connection: + - keep-alive + Host: + - api.upbit.com + User-Agent: + - python-httpx/0.28.1 + method: GET + uri: https://api.upbit.com/v1/orders/uuids?uuids%5B%5D=4f1d13a9-dc4d-4621-b1fe-7cb94f425006 + response: + body: + string: |- + [ + { + "uuid": "4f1d13a9-dc4d-4621-b1fe-7cb94f425006", + "side": "bid", + "ord_type": "limit", + "price": "10000000", + "state": "wait", + "market": "KRW-BTC", + "created_at": "2026-03-22T21:42:27+09:00", + "volume": "0.0005", + "remaining_volume": "0.0005", + "prevented_volume": "0", + "reserved_fee": "2.5", + "remaining_fee": "2.5", + "paid_fee": "0", + "locked": "5002.5", + "prevented_locked": "0", + "executed_volume": "0", + "executed_funds": "0", + "trades_count": 0 + } + ] + headers: + Cache-Control: + - no-cache, no-store, max-age=0, must-revalidate + Connection: + - keep-alive + Content-Type: + - application/json;charset=UTF-8 + Date: + - Sun, 22 Mar 2026 12:42:27 GMT + Remaining-Req: + - group=default; min=1800; sec=28 + Transfer-Encoding: + - chunked + Vary: + - Origin + - Access-Control-Request-Method + - Access-Control-Request-Headers + status: + code: 200 + message: OK +- request: + body: '' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - REDACTED + Connection: + - keep-alive + Host: + - api.upbit.com + User-Agent: + - python-httpx/0.28.1 + method: DELETE + uri: https://api.upbit.com/v1/order?uuid=4f1d13a9-dc4d-4621-b1fe-7cb94f425006 + response: + body: + string: |- + { + "uuid": "4f1d13a9-dc4d-4621-b1fe-7cb94f425006", + "side": "bid", + "ord_type": "limit", + "price": "10000000", + "state": "wait", + "market": "KRW-BTC", + "created_at": "2026-03-22T21:42:27+09:00", + "volume": "0.0005", + "remaining_volume": "0.0005", + "prevented_volume": "0", + "reserved_fee": "2.5", + "remaining_fee": "2.5", + "paid_fee": "0", + "locked": "5002.5", + "prevented_locked": "0", + "executed_volume": "0", + "trades_count": 0 + } + headers: + Cache-Control: + - no-cache, no-store, max-age=0, must-revalidate + Connection: + - keep-alive + Content-Type: + - application/json;charset=UTF-8 + Date: + - Sun, 22 Mar 2026 12:42:27 GMT + Remaining-Req: + - group=default; min=1800; sec=27 + Transfer-Encoding: + - chunked + Vary: + - Origin + - Access-Control-Request-Method + - Access-Control-Request-Headers + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/orders/list_closed.yaml b/tests/cassettes/orders/list_closed.yaml new file mode 100644 index 0000000..fad0781 --- /dev/null +++ b/tests/cassettes/orders/list_closed.yaml @@ -0,0 +1,324 @@ +interactions: +- request: + body: '' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - REDACTED + Connection: + - keep-alive + Host: + - api.upbit.com + User-Agent: + - python-httpx/0.28.1 + method: GET + uri: https://api.upbit.com/v1/orders/closed + response: + body: + string: |- + [ + { + "uuid": "a4337bbf-6d86-4361-bb34-180812fc87ce", + "side": "bid", + "ord_type": "limit", + "price": "10000000", + "state": "cancel", + "market": "KRW-BTC", + "created_at": "2026-03-22T21:36:34+09:00", + "volume": "0.0005", + "remaining_volume": "0.0005", + "prevented_volume": "0", + "reserved_fee": "2.5", + "remaining_fee": "2.5", + "paid_fee": "0", + "locked": "5002.5", + "prevented_locked": "0", + "executed_volume": "0", + "executed_funds": "0", + "trades_count": 0 + }, + { + "uuid": "56e7dbc5-35af-43bf-b8aa-c3361fb48e14", + "side": "bid", + "ord_type": "limit", + "price": "10100000", + "state": "cancel", + "market": "KRW-BTC", + "created_at": "2026-03-22T21:36:33+09:00", + "volume": "0.0005", + "remaining_volume": "0.0005", + "prevented_volume": "0", + "reserved_fee": "2.525", + "remaining_fee": "2.525", + "paid_fee": "0", + "locked": "5052.525", + "prevented_locked": "0", + "executed_volume": "0", + "executed_funds": "0", + "trades_count": 0 + }, + { + "uuid": "5443b788-252b-4e08-b467-408ff3c9009e", + "side": "bid", + "ord_type": "limit", + "price": "10000000", + "state": "cancel", + "market": "KRW-BTC", + "created_at": "2026-03-22T21:36:33+09:00", + "volume": "0.0005", + "remaining_volume": "0.0005", + "prevented_volume": "0", + "reserved_fee": "2.5", + "remaining_fee": "2.5", + "paid_fee": "0", + "locked": "5002.5", + "prevented_locked": "0", + "executed_volume": "0", + "executed_funds": "0", + "trades_count": 0 + }, + { + "uuid": "45b99b78-6e1e-4c29-95df-7a7b135c7963", + "side": "bid", + "ord_type": "limit", + "price": "10000000", + "state": "cancel", + "market": "KRW-BTC", + "created_at": "2026-03-22T21:36:32+09:00", + "volume": "0.0005", + "remaining_volume": "0.0005", + "prevented_volume": "0", + "reserved_fee": "2.5", + "remaining_fee": "2.5", + "paid_fee": "0", + "locked": "5002.5", + "prevented_locked": "0", + "executed_volume": "0", + "executed_funds": "0", + "trades_count": 0 + }, + { + "uuid": "65b4cf70-9036-4b37-8df3-d62824fb395d", + "side": "bid", + "ord_type": "limit", + "price": "10500000", + "state": "cancel", + "market": "KRW-BTC", + "created_at": "2026-03-22T21:36:31+09:00", + "volume": "0.0005", + "remaining_volume": "0.0005", + "prevented_volume": "0", + "reserved_fee": "2.625", + "remaining_fee": "2.625", + "paid_fee": "0", + "locked": "5252.625", + "prevented_locked": "0", + "executed_volume": "0", + "executed_funds": "0", + "trades_count": 0 + }, + { + "uuid": "766340f1-8d76-4fb5-a9c4-38077273a92e", + "side": "bid", + "ord_type": "limit", + "price": "10000000", + "state": "cancel", + "market": "KRW-BTC", + "created_at": "2026-03-22T21:36:31+09:00", + "volume": "0.0005", + "remaining_volume": "0.0005", + "prevented_volume": "0", + "reserved_fee": "2.5", + "remaining_fee": "2.5", + "paid_fee": "0", + "locked": "5002.5", + "prevented_locked": "0", + "executed_volume": "0", + "executed_funds": "0", + "trades_count": 0 + }, + { + "uuid": "c8ecd039-6b40-4112-8011-cbd1d58ef659", + "side": "bid", + "ord_type": "limit", + "price": "10000000", + "state": "cancel", + "market": "KRW-BTC", + "created_at": "2026-03-22T21:36:30+09:00", + "volume": "0.0005", + "remaining_volume": "0.0005", + "prevented_volume": "0", + "reserved_fee": "2.5", + "remaining_fee": "2.5", + "paid_fee": "0", + "locked": "5002.5", + "prevented_locked": "0", + "executed_volume": "0", + "executed_funds": "0", + "trades_count": 0 + }, + { + "uuid": "49be7a97-1f04-4e8d-bade-ccd51cd5a293", + "side": "bid", + "ord_type": "limit", + "price": "10000000", + "state": "cancel", + "market": "KRW-BTC", + "created_at": "2026-03-22T21:33:05+09:00", + "volume": "0.0005", + "remaining_volume": "0.0005", + "prevented_volume": "0", + "reserved_fee": "2.5", + "remaining_fee": "2.5", + "paid_fee": "0", + "locked": "5002.5", + "prevented_locked": "0", + "executed_volume": "0", + "executed_funds": "0", + "trades_count": 0 + }, + { + "uuid": "9823b686-109a-4487-b39f-2a94afea04eb", + "side": "bid", + "ord_type": "limit", + "price": "10100000", + "state": "cancel", + "market": "KRW-BTC", + "created_at": "2026-03-22T21:33:04+09:00", + "volume": "0.0005", + "remaining_volume": "0.0005", + "prevented_volume": "0", + "reserved_fee": "2.525", + "remaining_fee": "2.525", + "paid_fee": "0", + "locked": "5052.525", + "prevented_locked": "0", + "executed_volume": "0", + "executed_funds": "0", + "trades_count": 0 + }, + { + "uuid": "0dfbafd2-9527-447a-9714-d22481854791", + "side": "bid", + "ord_type": "limit", + "price": "10000000", + "state": "cancel", + "market": "KRW-BTC", + "created_at": "2026-03-22T21:33:03+09:00", + "volume": "0.0005", + "remaining_volume": "0.0005", + "prevented_volume": "0", + "reserved_fee": "2.5", + "remaining_fee": "2.5", + "paid_fee": "0", + "locked": "5002.5", + "prevented_locked": "0", + "executed_volume": "0", + "executed_funds": "0", + "trades_count": 0 + }, + { + "uuid": "e4a32d3d-c90b-4b5d-853d-3767b3607714", + "side": "bid", + "ord_type": "limit", + "price": "10000000", + "state": "cancel", + "market": "KRW-BTC", + "created_at": "2026-03-22T21:33:03+09:00", + "volume": "0.0005", + "remaining_volume": "0.0005", + "prevented_volume": "0", + "reserved_fee": "2.5", + "remaining_fee": "2.5", + "paid_fee": "0", + "locked": "5002.5", + "prevented_locked": "0", + "executed_volume": "0", + "executed_funds": "0", + "trades_count": 0 + }, + { + "uuid": "913cf8c2-7730-439a-9976-c1c77b953b1e", + "side": "bid", + "ord_type": "limit", + "price": "10500000", + "state": "cancel", + "market": "KRW-BTC", + "created_at": "2026-03-22T21:33:02+09:00", + "volume": "0.0005", + "remaining_volume": "0.0005", + "prevented_volume": "0", + "reserved_fee": "2.625", + "remaining_fee": "2.625", + "paid_fee": "0", + "locked": "5252.625", + "prevented_locked": "0", + "executed_volume": "0", + "executed_funds": "0", + "trades_count": 0 + }, + { + "uuid": "7723f2cd-57c7-478c-a5ab-515ef1e184f2", + "side": "bid", + "ord_type": "limit", + "price": "10000000", + "state": "cancel", + "market": "KRW-BTC", + "created_at": "2026-03-22T21:33:02+09:00", + "volume": "0.0005", + "remaining_volume": "0.0005", + "prevented_volume": "0", + "reserved_fee": "2.5", + "remaining_fee": "2.5", + "paid_fee": "0", + "locked": "5002.5", + "prevented_locked": "0", + "executed_volume": "0", + "executed_funds": "0", + "trades_count": 0 + }, + { + "uuid": "003b6bbd-684b-4921-a167-17cad043813f", + "side": "bid", + "ord_type": "limit", + "price": "10000000", + "state": "cancel", + "market": "KRW-BTC", + "created_at": "2026-03-22T21:33:01+09:00", + "volume": "0.0005", + "remaining_volume": "0.0005", + "prevented_volume": "0", + "reserved_fee": "2.5", + "remaining_fee": "2.5", + "paid_fee": "0", + "locked": "5002.5", + "prevented_locked": "0", + "executed_volume": "0", + "executed_funds": "0", + "trades_count": 0 + } + ] + headers: + Cache-Control: + - no-cache, no-store, max-age=0, must-revalidate + Connection: + - keep-alive + Content-Type: + - application/json;charset=UTF-8 + Date: + - Sun, 22 Mar 2026 12:42:20 GMT + Remaining-Req: + - group=default; min=1800; sec=29 + Transfer-Encoding: + - chunked + Vary: + - Origin + - Access-Control-Request-Method + - Access-Control-Request-Headers + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/orders/list_open.yaml b/tests/cassettes/orders/list_open.yaml new file mode 100644 index 0000000..153ed46 --- /dev/null +++ b/tests/cassettes/orders/list_open.yaml @@ -0,0 +1,42 @@ +interactions: +- request: + body: '' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - REDACTED + Connection: + - keep-alive + Host: + - api.upbit.com + User-Agent: + - python-httpx/0.28.1 + method: GET + uri: https://api.upbit.com/v1/orders/open + response: + body: + string: '[]' + headers: + Cache-Control: + - no-cache, no-store, max-age=0, must-revalidate + Connection: + - keep-alive + Content-Type: + - application/json;charset=UTF-8 + Date: + - Sun, 22 Mar 2026 12:42:16 GMT + Remaining-Req: + - group=default; min=1800; sec=29 + Transfer-Encoding: + - chunked + Vary: + - Origin + - Access-Control-Request-Method + - Access-Control-Request-Headers + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/orders/market_buy_sell.yaml b/tests/cassettes/orders/market_buy_sell.yaml new file mode 100644 index 0000000..9cd3d7b --- /dev/null +++ b/tests/cassettes/orders/market_buy_sell.yaml @@ -0,0 +1,262 @@ +interactions: +- request: + body: '{"market":"KRW-BTC","side":"bid","price":"10000","ord_type":"price"}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - REDACTED + Connection: + - keep-alive + Content-Length: + - '68' + Content-Type: + - application/json; charset=utf-8 + Host: + - api.upbit.com + User-Agent: + - python-httpx/0.28.1 + method: POST + uri: https://api.upbit.com/v1/orders + response: + body: + string: |- + { + "uuid": "fce8ffa4-c810-4446-946d-9fa2a6c80ab5", + "side": "bid", + "ord_type": "price", + "price": "10000", + "state": "wait", + "market": "KRW-BTC", + "created_at": "2026-03-22T21:47:08+09:00", + "reserved_fee": "5", + "remaining_fee": "5", + "paid_fee": "0", + "locked": "10005", + "prevented_locked": "0", + "executed_volume": "0", + "trades_count": 0 + } + headers: + Cache-Control: + - no-cache, no-store, max-age=0, must-revalidate + Connection: + - keep-alive + Content-Type: + - application/json;charset=UTF-8 + Date: + - Sun, 22 Mar 2026 12:47:08 GMT + Remaining-Req: + - group=order; min=480; sec=7 + Transfer-Encoding: + - chunked + Vary: + - Origin + - Access-Control-Request-Method + - Access-Control-Request-Headers + status: + code: 201 + message: Created +- request: + body: '' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - REDACTED + Connection: + - keep-alive + Host: + - api.upbit.com + User-Agent: + - python-httpx/0.28.1 + method: GET + uri: https://api.upbit.com/v1/order?uuid=fce8ffa4-c810-4446-946d-9fa2a6c80ab5 + response: + body: + string: |- + { + "uuid": "fce8ffa4-c810-4446-946d-9fa2a6c80ab5", + "side": "bid", + "ord_type": "price", + "price": "10000", + "state": "cancel", + "market": "KRW-BTC", + "created_at": "2026-03-22T21:47:08+09:00", + "reserved_fee": "5", + "remaining_fee": "0.000476075", + "paid_fee": "4.999523925", + "locked": "0.952626075", + "prevented_locked": "0", + "executed_volume": "0.00009713", + "trades_count": 1, + "trades": [ + { + "market": "KRW-BTC", + "uuid": "7dd003ce-cfff-4d68-a75b-a00e325b0661", + "price": "102945000", + "volume": "0.00009713", + "funds": "9999.04785", + "trend": "up", + "created_at": "2026-03-22T21:47:08+09:00", + "side": "bid" + } + ] + } + headers: + Cache-Control: + - no-cache, no-store, max-age=0, must-revalidate + Connection: + - keep-alive + Content-Type: + - application/json;charset=UTF-8 + Date: + - Sun, 22 Mar 2026 12:47:09 GMT + Remaining-Req: + - group=default; min=1800; sec=29 + Transfer-Encoding: + - chunked + Vary: + - Origin + - Access-Control-Request-Method + - Access-Control-Request-Headers + status: + code: 200 + message: OK +- request: + body: '{"market":"KRW-BTC","side":"ask","volume":"0.00009713","ord_type":"market"}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - REDACTED + Connection: + - keep-alive + Content-Length: + - '75' + Content-Type: + - application/json; charset=utf-8 + Host: + - api.upbit.com + User-Agent: + - python-httpx/0.28.1 + method: POST + uri: https://api.upbit.com/v1/orders + response: + body: + string: |- + { + "uuid": "72091b0f-7d13-4a1e-aec5-1105a84783cd", + "side": "ask", + "ord_type": "market", + "state": "wait", + "market": "KRW-BTC", + "created_at": "2026-03-22T21:47:09+09:00", + "volume": "0.00009713", + "remaining_volume": "0.00009713", + "prevented_volume": "0", + "reserved_fee": "0", + "remaining_fee": "0", + "paid_fee": "0", + "locked": "0.00009713", + "prevented_locked": "0", + "executed_volume": "0", + "trades_count": 0 + } + headers: + Cache-Control: + - no-cache, no-store, max-age=0, must-revalidate + Connection: + - keep-alive + Content-Type: + - application/json;charset=UTF-8 + Date: + - Sun, 22 Mar 2026 12:47:09 GMT + Remaining-Req: + - group=order; min=480; sec=7 + Transfer-Encoding: + - chunked + Vary: + - Origin + - Access-Control-Request-Method + - Access-Control-Request-Headers + status: + code: 201 + message: Created +- request: + body: '' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Authorization: + - REDACTED + Connection: + - keep-alive + Host: + - api.upbit.com + User-Agent: + - python-httpx/0.28.1 + method: GET + uri: https://api.upbit.com/v1/order?uuid=72091b0f-7d13-4a1e-aec5-1105a84783cd + response: + body: + string: |- + { + "uuid": "72091b0f-7d13-4a1e-aec5-1105a84783cd", + "side": "ask", + "ord_type": "market", + "state": "done", + "market": "KRW-BTC", + "created_at": "2026-03-22T21:47:09+09:00", + "volume": "0.00009713", + "remaining_volume": "0", + "prevented_volume": "0", + "reserved_fee": "0", + "remaining_fee": "0", + "paid_fee": "4.99743563", + "locked": "0", + "prevented_locked": "0", + "executed_volume": "0.00009713", + "trades_count": 1, + "trades": [ + { + "market": "KRW-BTC", + "uuid": "5990f027-5856-4b90-be14-3a30e34b764c", + "price": "102902000", + "volume": "0.00009713", + "funds": "9994.87126", + "trend": "down", + "created_at": "2026-03-22T21:47:09+09:00", + "side": "ask" + } + ] + } + headers: + Cache-Control: + - no-cache, no-store, max-age=0, must-revalidate + Connection: + - keep-alive + Content-Type: + - application/json;charset=UTF-8 + Date: + - Sun, 22 Mar 2026 12:47:10 GMT + Remaining-Req: + - group=default; min=1800; sec=29 + Transfer-Encoding: + - chunked + Vary: + - Origin + - Access-Control-Request-Method + - Access-Control-Request-Headers + status: + code: 200 + message: OK +version: 1 From c54a00ce916470157d57859d011656600082ae2c Mon Sep 17 00:00:00 2001 From: gylim Date: Sun, 22 Mar 2026 21:56:10 +0900 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20record=5Forder=5Fcassettes.py=20?= =?UTF-8?q?=EC=A3=BC=EC=84=9D=EA=B3=BC=20=EC=BD=94=EB=93=9C=20=EB=B6=88?= =?UTF-8?q?=EC=9D=BC=EC=B9=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 매수 금액을 5000→10000원으로 변경한 후 docstring과 예상 손실 안내가 업데이트되지 않았던 부분 수정. Co-Authored-By: Claude Opus 4.6 (1M context) --- scripts/record_order_cassettes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/record_order_cassettes.py b/scripts/record_order_cassettes.py index 9cf48b4..aa0b124 100644 --- a/scripts/record_order_cassettes.py +++ b/scripts/record_order_cassettes.py @@ -1,7 +1,7 @@ """시장가 매수→매도 VCR cassette 녹화 스크립트. 실제 거래가 발생하므로 소량의 금전적 손실(스프레드+수수료)이 발생한다. -KRW-BTC 마켓에서 최소금액(5,000원)으로 매수 후 즉시 전량 매도한다. +KRW-BTC 마켓에서 10,000원으로 매수 후 즉시 전량 매도한다. 사용법: UPBIT_ACCESS_KEY=xxx UPBIT_SECRET_KEY=yyy \ @@ -43,7 +43,7 @@ def main() -> None: print("=" * 60) print(f" 마켓: {_MARKET}") print(f" 매수 금액: {_BUY_AMOUNT}원") - print(" 예상 손실: 스프레드 + 수수료 ≈ 50~100원") + print(" 예상 손실: 스프레드 + 수수료 ≈ 100~200원") print(f" Cassette: {_CASSETTE_PATH}") print("=" * 60) print()