From a357de75df95a72dce672f6610dfc9c67409eb28 Mon Sep 17 00:00:00 2001 From: Geronimo Date: Mon, 17 Jun 2024 13:33:07 -0400 Subject: [PATCH 01/17] add async order creation and message sending --- hundred_x/async_client.py | 56 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/hundred_x/async_client.py b/hundred_x/async_client.py index a6e5f6f..9e3a31a 100644 --- a/hundred_x/async_client.py +++ b/hundred_x/async_client.py @@ -1,15 +1,69 @@ """ Async client for the HundredX API """ +import httpx +import requests +from decimal import Decimal from hundred_x.client import HundredXClient - +from hundred_x.eip_712 import Order +from hundred_x.enums import OrderSide, OrderType, TimeInForce +from hundred_x.utils import from_message_to_payload, get_abi class AsyncHundredXClient(HundredXClient): """ Asynchronous client for the HundredX API. """ + async def create_and_send_order( + self, + subaccount_id: int, + product_id: int, + quantity: int, + price: int, + side: OrderSide, + order_type: OrderType, + time_in_force: TimeInForce, + nonce: int = 0, + ): + """ + Create and send order. + """ + ts = self._current_timestamp() + if nonce == 0: + nonce = ts + message = self.generate_and_sign_message( + Order, + subAccountId=subaccount_id, + productId=product_id, + quantity=int(Decimal(str(quantity)) * Decimal(1e18)), + price=int(Decimal(str(price)) * Decimal(1e18)), + isBuy=side.value, + orderType=order_type.value, + timeInForce=time_in_force.value, + nonce=nonce, + expiration=(ts + 1000 * 60 * 60 * 24) * 1000, + **self.get_shared_params(), + ) + response = await self.send_message_to_endpoint("/v1/order", "POST", message) + return response + + async def send_message_to_endpoint(self, endpoint: str, method: str, message: dict, authenticated: bool = True): + """ + Send a message to an endpoint. + """ + payload = from_message_to_payload(message) + async with httpx.AsyncClient() as client: + response = await client.request( + method, + self.rest_url + endpoint, + headers={} if not authenticated else self.authenticated_headers, + json=payload, + ) + if response.status_code != 200: + raise Exception(f"Failed to send message: {response.text} {response.status_code} {self.rest_url} {payload}") + return response.json() + async def list_products(self): """ List all products available on the exchange. From 1f2e7215a6c9c2a5b26e395eb4e164445278af54 Mon Sep 17 00:00:00 2001 From: Geronimo Date: Mon, 17 Jun 2024 13:44:10 -0400 Subject: [PATCH 02/17] add cancel and replace order async --- hundred_x/async_client.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/hundred_x/async_client.py b/hundred_x/async_client.py index 9e3a31a..c8086e1 100644 --- a/hundred_x/async_client.py +++ b/hundred_x/async_client.py @@ -48,6 +48,41 @@ async def create_and_send_order( response = await self.send_message_to_endpoint("/v1/order", "POST", message) return response + async def cancel_and_replace_order( + self, + subaccount_id: int, + product_id: int, + quantity: int, + price: int, + side: OrderSide, + order_id_to_cancel: str, + nonce: int = 0, + ): + """ + Cancel and replace an order. + """ + ts = self._current_timestamp() + if nonce == 0: + nonce = ts + _message = self.generate_and_sign_message( + Order, + subAccountId=subaccount_id, + productId=product_id, + quantity=int(Decimal(str(quantity)) * Decimal(1e18)), + price=int(Decimal(str(price)) * Decimal(1e18)), + isBuy=side.value, + orderType=OrderType.LIMIT_MAKER.value, + timeInForce=TimeInForce.GTC.value, + nonce=nonce, + expiration=(ts + 1000 * 60 * 60 * 24) * 1000, + **self.get_shared_params(), + ) + message = {} + message["newOrder"] = from_message_to_payload(_message) + message["idToCancel"] = order_id_to_cancel + response = await self.send_message_to_endpoint("/v1/order/cancel-and-replace", "POST", message) + return response + async def send_message_to_endpoint(self, endpoint: str, method: str, message: dict, authenticated: bool = True): """ Send a message to an endpoint. From 5b5d13445c36e1b2adde04775faf728d9f20545f Mon Sep 17 00:00:00 2001 From: Geronimo Date: Mon, 17 Jun 2024 14:00:24 -0400 Subject: [PATCH 03/17] add httpx --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 9851e47..35f7be6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,7 @@ eip712-structs-ng = "^2.0.1" web3 = ">=6,<7" websockets = ">=9" safe-pysha3 = "^1.0.4" +httpx = "^0.27.0" [tool.poetry.group.dev.dependencies] From dfee9e5b0a92fd8afdc176e0c209107ef7695af6 Mon Sep 17 00:00:00 2001 From: Geronimo Date: Tue, 18 Jun 2024 15:51:55 -0400 Subject: [PATCH 04/17] add cancel_all_orders async --- hundred_x/async_client.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/hundred_x/async_client.py b/hundred_x/async_client.py index c8086e1..86a4837 100644 --- a/hundred_x/async_client.py +++ b/hundred_x/async_client.py @@ -6,7 +6,7 @@ from decimal import Decimal from hundred_x.client import HundredXClient -from hundred_x.eip_712 import Order +from hundred_x.eip_712 import Order, CancelOrders from hundred_x.enums import OrderSide, OrderType, TimeInForce from hundred_x.utils import from_message_to_payload, get_abi @@ -82,6 +82,19 @@ async def cancel_and_replace_order( message["idToCancel"] = order_id_to_cancel response = await self.send_message_to_endpoint("/v1/order/cancel-and-replace", "POST", message) return response + + async def cancel_all_orders(self, subaccount_id: int, product_id: int): + """ + Cancel all orders. + """ + message = self.generate_and_sign_message( + CancelOrders, + subAccountId=subaccount_id, + productId=product_id, + **self.get_shared_params(), + ) + response = await self.send_message_to_endpoint("/v1/openOrders", "DELETE", message) + return response async def send_message_to_endpoint(self, endpoint: str, method: str, message: dict, authenticated: bool = True): """ From 746cd15a33fc0d8cd7de3a5302ad498fa617d292 Mon Sep 17 00:00:00 2001 From: Geronimo Date: Thu, 20 Jun 2024 11:45:04 -0400 Subject: [PATCH 05/17] add cancel order --- hundred_x/async_client.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/hundred_x/async_client.py b/hundred_x/async_client.py index 86a4837..fab00fa 100644 --- a/hundred_x/async_client.py +++ b/hundred_x/async_client.py @@ -6,7 +6,7 @@ from decimal import Decimal from hundred_x.client import HundredXClient -from hundred_x.eip_712 import Order, CancelOrders +from hundred_x.eip_712 import Order, CancelOrders, CancelOrder from hundred_x.enums import OrderSide, OrderType, TimeInForce from hundred_x.utils import from_message_to_payload, get_abi @@ -48,6 +48,20 @@ async def create_and_send_order( response = await self.send_message_to_endpoint("/v1/order", "POST", message) return response + async def cancel_order(self, subaccount_id: int, product_id: int, order_id: int): + """ + Cancel an order. + """ + message = self.generate_and_sign_message( + CancelOrder, + subAccountId=subaccount_id, + productId=product_id, + orderId=order_id, + **self.get_shared_params(), + ) + response = await self.send_message_to_endpoint("/v1/order", "DELETE", message) + return response + async def cancel_and_replace_order( self, subaccount_id: int, From 0abd232921355383d673e4af35a83a20e5502b70 Mon Sep 17 00:00:00 2001 From: Geronimo Date: Thu, 20 Jun 2024 12:42:30 -0400 Subject: [PATCH 06/17] add get position and spot balances async --- hundred_x/async_client.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/hundred_x/async_client.py b/hundred_x/async_client.py index fab00fa..6683333 100644 --- a/hundred_x/async_client.py +++ b/hundred_x/async_client.py @@ -15,6 +15,27 @@ class AsyncHundredXClient(HundredXClient): Asynchronous client for the HundredX API. """ + async def get_position(self, symbol: str): + """ + Get the position of the given symbol. + """ + message = { + "symbol": symbol, + "account": self.public_key, + "subAccountId": self.subaccount_id, + } + return await self.send_message_to_endpoint("/v1/positionRisk", "GET", message) + + async def get_spot_balances(self): + """ + Get the spot balances. + """ + message = { + "account": self.public_key, + "subAccountId": self.subaccount_id, + } + return await self.send_message_to_endpoint("/v1/balances", "GET", message) + async def create_and_send_order( self, subaccount_id: int, From bbde0cc23d50d62ed00ea7185df9c0098eed712f Mon Sep 17 00:00:00 2001 From: Geronimo Date: Thu, 20 Jun 2024 15:06:39 -0400 Subject: [PATCH 07/17] add async login --- hundred_x/async_client.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/hundred_x/async_client.py b/hundred_x/async_client.py index 6683333..6443c59 100644 --- a/hundred_x/async_client.py +++ b/hundred_x/async_client.py @@ -6,7 +6,8 @@ from decimal import Decimal from hundred_x.client import HundredXClient -from hundred_x.eip_712 import Order, CancelOrders, CancelOrder +from hundred_x.eip_712 import Order, CancelOrders, CancelOrder, LoginMessage +from hundred_x.constants import LOGIN_MESSAGE from hundred_x.enums import OrderSide, OrderType, TimeInForce from hundred_x.utils import from_message_to_payload, get_abi @@ -130,6 +131,14 @@ async def cancel_all_orders(self, subaccount_id: int, product_id: int): ) response = await self.send_message_to_endpoint("/v1/openOrders", "DELETE", message) return response + + async def login(self): + """ + Login to the exchange. + """ + response = await self.create_authenticated_session_with_service() + if response is None: + raise Exception("Failed to login") async def send_message_to_endpoint(self, endpoint: str, method: str, message: dict, authenticated: bool = True): """ @@ -147,6 +156,17 @@ async def send_message_to_endpoint(self, endpoint: str, method: str, message: di raise Exception(f"Failed to send message: {response.text} {response.status_code} {self.rest_url} {payload}") return response.json() + async def create_authenticated_session_with_service(self): + login_payload = self.generate_and_sign_message( + LoginMessage, + message=LOGIN_MESSAGE, + timestamp=self._current_timestamp(), + **self.get_shared_params(), + ) + response = await self.send_message_to_endpoint("/v1/session/login", "POST", login_payload, authenticated=False) + self.session_cookie = response.get("value") + return response + async def list_products(self): """ List all products available on the exchange. From 524613be3f91cba83f8c12f5f7729d766a546c13 Mon Sep 17 00:00:00 2001 From: Geronimo Date: Thu, 20 Jun 2024 15:45:01 -0400 Subject: [PATCH 08/17] update send to endpoint --- hundred_x/async_client.py | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/hundred_x/async_client.py b/hundred_x/async_client.py index 6443c59..478f3c0 100644 --- a/hundred_x/async_client.py +++ b/hundred_x/async_client.py @@ -18,14 +18,19 @@ class AsyncHundredXClient(HundredXClient): async def get_position(self, symbol: str): """ - Get the position of the given symbol. + Get the position for a specific symbol. """ message = { "symbol": symbol, "account": self.public_key, "subAccountId": self.subaccount_id, } - return await self.send_message_to_endpoint("/v1/positionRisk", "GET", message) + return await self.send_message_to_endpoint( + endpoint="/v1/positionRisk", + method="GET", + message=message, + authenticated=True, + ) async def get_spot_balances(self): """ @@ -146,12 +151,21 @@ async def send_message_to_endpoint(self, endpoint: str, method: str, message: di """ payload = from_message_to_payload(message) async with httpx.AsyncClient() as client: - response = await client.request( - method, - self.rest_url + endpoint, - headers={} if not authenticated else self.authenticated_headers, - json=payload, - ) + if method.upper() == "GET": + response = await client.request( + method, + self.rest_url + endpoint, + headers={} if not authenticated else self.authenticated_headers, + params=payload, + ) + else: + response = await client.request( + method, + self.rest_url + endpoint, + headers={} if not authenticated else self.authenticated_headers, + json=payload, + ) + if response.status_code != 200: raise Exception(f"Failed to send message: {response.text} {response.status_code} {self.rest_url} {payload}") return response.json() From 19dbb8346ee29ab3d0b0dc19a189932513758ce7 Mon Sep 17 00:00:00 2001 From: Geronimo Date: Sun, 23 Jun 2024 11:18:37 -0400 Subject: [PATCH 09/17] add get open orders async --- hundred_x/async_client.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/hundred_x/async_client.py b/hundred_x/async_client.py index 478f3c0..431c512 100644 --- a/hundred_x/async_client.py +++ b/hundred_x/async_client.py @@ -42,6 +42,15 @@ async def get_spot_balances(self): } return await self.send_message_to_endpoint("/v1/balances", "GET", message) + async def get_open_orders(self, symbol: str = None): + """ + Get the open orders for a specific symbol. + """ + params = {"symbol": symbol, "account": self.public_key, "subAccountId": self.subaccount_id} + if symbol is None: + del params["symbol"] + return await self.send_message_to_endpoint("/v1/orders", "GET", params) + async def create_and_send_order( self, subaccount_id: int, From 1b9d176b88877e94c3d7f9c040fee5fe35bbd171 Mon Sep 17 00:00:00 2001 From: Geronimo Date: Sun, 23 Jun 2024 11:40:24 -0400 Subject: [PATCH 10/17] use openOrders --- hundred_x/async_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hundred_x/async_client.py b/hundred_x/async_client.py index 431c512..6d73ec5 100644 --- a/hundred_x/async_client.py +++ b/hundred_x/async_client.py @@ -49,7 +49,7 @@ async def get_open_orders(self, symbol: str = None): params = {"symbol": symbol, "account": self.public_key, "subAccountId": self.subaccount_id} if symbol is None: del params["symbol"] - return await self.send_message_to_endpoint("/v1/orders", "GET", params) + return await self.send_message_to_endpoint("/v1/openOrders", "GET", params) async def create_and_send_order( self, From 49d50d1f041c9ddad6a51a0f4da4fc497c9dd241 Mon Sep 17 00:00:00 2001 From: Geronimo Date: Sun, 23 Jun 2024 15:58:52 -0400 Subject: [PATCH 11/17] remove subacount param --- hundred_x/async_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hundred_x/async_client.py b/hundred_x/async_client.py index 6d73ec5..a4a6755 100644 --- a/hundred_x/async_client.py +++ b/hundred_x/async_client.py @@ -84,13 +84,13 @@ async def create_and_send_order( response = await self.send_message_to_endpoint("/v1/order", "POST", message) return response - async def cancel_order(self, subaccount_id: int, product_id: int, order_id: int): + async def cancel_order(self, product_id: int, order_id: int): """ Cancel an order. """ message = self.generate_and_sign_message( CancelOrder, - subAccountId=subaccount_id, + subAccountId=self.subaccount_id, productId=product_id, orderId=order_id, **self.get_shared_params(), From 1f6a7b342c0d523d3a43ab79ad7a84d2a429a8d3 Mon Sep 17 00:00:00 2001 From: Geronimo Date: Tue, 25 Jun 2024 06:33:35 -0400 Subject: [PATCH 12/17] add get symbol_info --- hundred_x/async_client.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/hundred_x/async_client.py b/hundred_x/async_client.py index a4a6755..027942c 100644 --- a/hundred_x/async_client.py +++ b/hundred_x/async_client.py @@ -16,6 +16,16 @@ class AsyncHundredXClient(HundredXClient): Asynchronous client for the HundredX API. """ + async def get_symbol_info(self, symbol: str): + """ + Get the symbol info for a specific symbol. + """ + response = await self.send_message_to_endpoint( + endpoint=f"/v1/ticker/24hr?symbol={symbol}", + method="GET", + ) + return response[0] + async def get_position(self, symbol: str): """ Get the position for a specific symbol. @@ -154,7 +164,7 @@ async def login(self): if response is None: raise Exception("Failed to login") - async def send_message_to_endpoint(self, endpoint: str, method: str, message: dict, authenticated: bool = True): + async def send_message_to_endpoint(self, endpoint: str, method: str, message: dict = {}, authenticated: bool = True): """ Send a message to an endpoint. """ From d0e49b311095c69a2565dca5802e1b2fe2c12a15 Mon Sep 17 00:00:00 2001 From: Geronimo Date: Tue, 25 Jun 2024 06:41:57 -0400 Subject: [PATCH 13/17] add get_depth async --- hundred_x/async_client.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/hundred_x/async_client.py b/hundred_x/async_client.py index 027942c..4dd171d 100644 --- a/hundred_x/async_client.py +++ b/hundred_x/async_client.py @@ -1,6 +1,7 @@ """ Async client for the HundredX API """ +from typing import Any import httpx import requests from decimal import Decimal @@ -25,6 +26,20 @@ async def get_symbol_info(self, symbol: str): method="GET", ) return response[0] + + async def get_depth(self, symbol: str, **kwargs) -> Any: + """ + Get the depth for a specific symbol. + """ + params = { + "symbol": symbol, + "limit": kwargs.get("limit", 5), + } + return await self.send_message_to_endpoint( + endpoint=f"/v1/depth", + method="GET", + message=params, + ) async def get_position(self, symbol: str): """ From 5809db5c639b9ccb6f11b1219c1035c64f6e0e25 Mon Sep 17 00:00:00 2001 From: Geronimo Date: Tue, 25 Jun 2024 10:54:43 -0400 Subject: [PATCH 14/17] add get trades async --- hundred_x/async_client.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/hundred_x/async_client.py b/hundred_x/async_client.py index 4dd171d..16671f8 100644 --- a/hundred_x/async_client.py +++ b/hundred_x/async_client.py @@ -41,6 +41,20 @@ async def get_depth(self, symbol: str, **kwargs) -> Any: message=params, ) + async def get_trade_history(self, symbol: str, lookback: int) -> Any: + """ + Get the trade history for a specific symbol. + """ + params = { + "symbol": symbol, + "limit": lookback, + } + return await self.send_message_to_endpoint( + endpoint=f"/v1/trade-history", + method="GET", + message=params, + ) + async def get_position(self, symbol: str): """ Get the position for a specific symbol. From 77fcb5a76c3b9d3f887a3ca5172a82e687487172 Mon Sep 17 00:00:00 2001 From: Geronimo Date: Tue, 25 Jun 2024 14:17:06 -0400 Subject: [PATCH 15/17] remove sync functions labeled async This is not needed since it inherits from the sync class --- hundred_x/async_client.py | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/hundred_x/async_client.py b/hundred_x/async_client.py index 16671f8..8c7858e 100644 --- a/hundred_x/async_client.py +++ b/hundred_x/async_client.py @@ -227,22 +227,4 @@ async def create_authenticated_session_with_service(self): ) response = await self.send_message_to_endpoint("/v1/session/login", "POST", login_payload, authenticated=False) self.session_cookie = response.get("value") - return response - - async def list_products(self): - """ - List all products available on the exchange. - """ - return super().list_products() - - async def get_product(self, symbol: str): - """ - Get a specific product available on the exchange. - """ - return super().get_product(symbol) - - async def get_server_time(self): - """ - Get the server time. - """ - return super().get_server_time() + return response \ No newline at end of file From 4a7f156fc7361478b8723267f2f738f816191de6 Mon Sep 17 00:00:00 2001 From: Geronimo Date: Tue, 25 Jun 2024 14:37:13 -0400 Subject: [PATCH 16/17] add async_client tests --- pyproject.toml | 2 + tests/test_async_hundred_x_client.py | 92 ++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 tests/test_async_hundred_x_client.py diff --git a/pyproject.toml b/pyproject.toml index 35f7be6..e99965d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,8 @@ web3 = ">=6,<7" websockets = ">=9" safe-pysha3 = "^1.0.4" httpx = "^0.27.0" +pytest-asyncio = "^0.23.7" +respx = "^0.21.1" [tool.poetry.group.dev.dependencies] diff --git a/tests/test_async_hundred_x_client.py b/tests/test_async_hundred_x_client.py new file mode 100644 index 0000000..7ebf776 --- /dev/null +++ b/tests/test_async_hundred_x_client.py @@ -0,0 +1,92 @@ +import pytest +import respx +from httpx import Response +from decimal import Decimal +from hundred_x.enums import OrderSide, OrderType, TimeInForce +from hundred_x.async_client import AsyncHundredXClient +from hundred_x.enums import Environment + +TEST_PRIVATE_KEY = "0x8f58e47491ac5fe6897216208fe1fed316d6ee89de6c901bfc521c2178ebe6dd" + +@pytest.fixture +def client(): + env = Environment.PROD + return AsyncHundredXClient(env, TEST_PRIVATE_KEY) + +BASE_URL = "https://api.100x.finance" + +@pytest.mark.asyncio +@respx.mock +async def test_get_symbol_info(client): + symbol = "BTCUSD" + respx.get(f"{BASE_URL}/v1/ticker/24hr", params={"symbol": symbol}).mock( + return_value=Response(200, json=[{"symbol": symbol, "price": "50000.00"}]) + ) + response = await client.get_symbol_info(symbol) + assert response["symbol"] == symbol + assert response["price"] == "50000.00" + +@pytest.mark.asyncio +@respx.mock +async def test_get_depth(client): + symbol = "BTCUSD" + respx.get(f"{BASE_URL}/v1/depth", params={"symbol": symbol, "limit": 5}).mock( + return_value=Response(200, json={"bids": [], "asks": []}) + ) + response = await client.get_depth(symbol) + assert "bids" in response + assert "asks" in response + +@pytest.mark.asyncio +@respx.mock +async def test_get_trade_history(client): + symbol = "BTCUSD" + lookback = 10 + respx.get(f"{BASE_URL}/v1/trade-history", params={"symbol": symbol, "limit": lookback}).mock( + return_value=Response(200, json=[{"id": 1, "price": "50000.00"}]) + ) + response = await client.get_trade_history(symbol, lookback) + assert isinstance(response, list) + assert response[0]["id"] == 1 + assert response[0]["price"] == "50000.00" + +@pytest.mark.asyncio +@respx.mock +async def test_create_and_send_order(client): + subaccount_id = 1 + product_id = 1 + quantity = 1 + price = 50000 + side = OrderSide.BUY + order_type = OrderType.LIMIT + time_in_force = TimeInForce.GTC + respx.post(f"{BASE_URL}/v1/order").mock( + return_value=Response(200, json={"orderId": "12345"}) + ) + response = await client.create_and_send_order( + subaccount_id, product_id, quantity, price, side, order_type, time_in_force + ) + assert response["orderId"] == "12345" + +@pytest.mark.asyncio +@respx.mock +async def test_cancel_order(client): + product_id = 1 + order_id = "12345" # Changing order_id to string + respx.delete(f"{BASE_URL}/v1/order").mock( + return_value=Response(200, json={"status": "success"}) + ) + response = await client.cancel_order(product_id, order_id) + assert response["status"] == "success" + +@pytest.mark.asyncio +@respx.mock +async def test_get_open_orders(client): + symbol = "BTCUSD" + respx.get(f"{BASE_URL}/v1/openOrders", params={"symbol": symbol, "account": client.public_key, "subAccountId": client.subaccount_id}).mock( + return_value=Response(200, json=[{"orderId": "12345", "symbol": symbol}]) + ) + response = await client.get_open_orders(symbol) + assert isinstance(response, list) + assert response[0]["orderId"] == "12345" + assert response[0]["symbol"] == symbol \ No newline at end of file From 0d19e20d8d8f0d245346883e9582d54de201cfc4 Mon Sep 17 00:00:00 2001 From: 8baller Date: Mon, 1 Jul 2024 10:19:06 +0000 Subject: [PATCH 17/17] feat: ensured all async etc --- .gitignore | 1 + README.md | 1 + examples/run_client.py | 105 ++++++++++++++ hundred_x/async_client.py | 205 +++++++-------------------- hundred_x/client.py | 91 +++++++----- poetry.lock | 203 +++++++++++++++++++++++++- pyproject.toml | 6 +- tests/__init__.py | 0 tests/test_async_hundred_x_client.py | 106 +++++++------- tests/test_data.py | 3 + 10 files changed, 476 insertions(+), 245 deletions(-) create mode 100644 examples/run_client.py create mode 100644 tests/__init__.py diff --git a/.gitignore b/.gitignore index 6afae43..c7f128e 100644 --- a/.gitignore +++ b/.gitignore @@ -115,5 +115,6 @@ site/ .history *trader +agent node_modules diff --git a/README.md b/README.md index 8b13547..d6c784c 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ price = client.get_product(DEFAULT_SYMBOL) print(price) ``` +Fopr a demonstration of the async client please refer to the file in examples/async_client.py ## Installation diff --git a/examples/run_client.py b/examples/run_client.py new file mode 100644 index 0000000..1e9ce6c --- /dev/null +++ b/examples/run_client.py @@ -0,0 +1,105 @@ +""" +Example of a client that connects to a server and sends a message. +""" + +import asyncio +import os +from pprint import pprint + +from hundred_x.async_client import AsyncHundredXClient +from hundred_x.enums import Environment, OrderSide, OrderType, TimeInForce + + +async def main(): + key = os.getenv("HUNDRED_X_PRIVATE_KEY") + subaccount_id = 0 + + if not key: + raise ValueError("HUNDRED_X_PRIVATE_KEY environment variable is not set.") + + client = AsyncHundredXClient(Environment.PROD, key, subaccount_id=subaccount_id) + + print(f"Using Wallet: {client.public_key}") + print(f"Using Subaccount ID: {client.subaccount_id}") + + # In order to hit the authenticated endpoints, we need to login. + await client.login() + + # We check the initial balance. + print("Initial Balance") + response = await client.get_spot_balances() + pprint(response) + + # We first get the symbol. + print("Symbol") + response = await client.get_symbol("btcperp") + pprint(response) + + # We create an order. + print("Create Order") + response = await client.create_order( + subaccount_id=subaccount_id, + product_id=response['productId'], # This is the product_id for the symbol 'btcperp + quantity=0.001, + price=round(int(response['markPrice']) * 0.99 / 1e18), # This is the current price of the symbol 'btcperp' + side=OrderSide.BUY, + order_type=OrderType.LIMIT, + time_in_force=TimeInForce.GTC, + ) + + # We check the open orders. + print("Open Orders") + response = await client.get_open_orders("btcperp") + pprint(response) + + # We cancel the order. + print("Cancel Order") + response = await client.cancel_order(order_id=response[0]["id"], product_id=response[0]["productId"]) + pprint(response) + + # We check the positions. + print("Positions") + response = await client.get_position() + pprint(response) + + # We check if we can cancel and replace an order. + # We check the open orders. + print("Open Orders") + response = await client.get_open_orders("btcperp") + pprint(response) + + # We create an order. + print("Create Order") + + response = await client.create_order( + subaccount_id=subaccount_id, + product_id=response[0]["productId"], + quantity=0.001, + price=round(int(response[0]["price"]) * 0.99 / 1e18), + side=OrderSide.BUY, + order_type=OrderType.LIMIT, + time_in_force=TimeInForce.GTC, + ) + + # We check the open orders. + print("Open Orders") + response = await client.get_open_orders("btcperp") + pprint(response) + + # We cancel and replace the order. + print("Cancel and Replace Order") + response = await client.cancel_and_replace_order( + order_id_to_cancel=response[0]["id"], + product_id=response[0]["productId"], + quantity=0.002, + price=round(int(response[0]["price"]) * 0.99 / 1e18), + side=OrderSide.BUY, + ) + pprint(response) + + # We cancel all orders. + response = await client.cancel_all_orders(subaccount_id=subaccount_id, product_id=1002) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/hundred_x/async_client.py b/hundred_x/async_client.py index 8c7858e..21ec2e1 100644 --- a/hundred_x/async_client.py +++ b/hundred_x/async_client.py @@ -1,190 +1,89 @@ """ Async client for the HundredX API """ + from typing import Any + import httpx -import requests -from decimal import Decimal from hundred_x.client import HundredXClient -from hundred_x.eip_712 import Order, CancelOrders, CancelOrder, LoginMessage from hundred_x.constants import LOGIN_MESSAGE -from hundred_x.enums import OrderSide, OrderType, TimeInForce -from hundred_x.utils import from_message_to_payload, get_abi +from hundred_x.eip_712 import LoginMessage +from hundred_x.exceptions import ClientError +from hundred_x.utils import from_message_to_payload + class AsyncHundredXClient(HundredXClient): """ Asynchronous client for the HundredX API. """ - async def get_symbol_info(self, symbol: str): + async def get_symbol(self, symbol: str = None): """ - Get the symbol info for a specific symbol. + Get the symbol infos. + If symbol is None, return all symbols. """ - response = await self.send_message_to_endpoint( - endpoint=f"/v1/ticker/24hr?symbol={symbol}", - method="GET", - ) - return response[0] - + response = await super().get_symbol(symbol) + if symbol: + return response[0] + else: + return response + async def get_depth(self, symbol: str, **kwargs) -> Any: """ Get the depth for a specific symbol. """ - params = { - "symbol": symbol, - "limit": kwargs.get("limit", 5), - } - return await self.send_message_to_endpoint( - endpoint=f"/v1/depth", - method="GET", - message=params, - ) + return await super().get_depth(symbol, **kwargs) - async def get_trade_history(self, symbol: str, lookback: int) -> Any: + async def get_trade_history(self, symbol: str, lookback: int = 10, **kwargs) -> Any: """ Get the trade history for a specific symbol. + if symbol is None, return all trade history. """ - params = { - "symbol": symbol, - "limit": lookback, - } - return await self.send_message_to_endpoint( - endpoint=f"/v1/trade-history", - method="GET", - message=params, - ) + return await super().get_trade_history(symbol, lookback, **kwargs) - async def get_position(self, symbol: str): + async def get_position(self, symbol: str = None): """ Get the position for a specific symbol. """ - message = { - "symbol": symbol, - "account": self.public_key, - "subAccountId": self.subaccount_id, - } - return await self.send_message_to_endpoint( - endpoint="/v1/positionRisk", - method="GET", - message=message, - authenticated=True, - ) + return await super().get_position(symbol) async def get_spot_balances(self): """ Get the spot balances. """ - message = { - "account": self.public_key, - "subAccountId": self.subaccount_id, - } - return await self.send_message_to_endpoint("/v1/balances", "GET", message) + return await super().get_spot_balances() async def get_open_orders(self, symbol: str = None): """ Get the open orders for a specific symbol. """ - params = {"symbol": symbol, "account": self.public_key, "subAccountId": self.subaccount_id} - if symbol is None: - del params["symbol"] - return await self.send_message_to_endpoint("/v1/openOrders", "GET", params) - - async def create_and_send_order( - self, - subaccount_id: int, - product_id: int, - quantity: int, - price: int, - side: OrderSide, - order_type: OrderType, - time_in_force: TimeInForce, - nonce: int = 0, - ): + return await super().get_open_orders(symbol) + + async def create_order(self, *args, **kwargs): """ Create and send order. """ - ts = self._current_timestamp() - if nonce == 0: - nonce = ts - message = self.generate_and_sign_message( - Order, - subAccountId=subaccount_id, - productId=product_id, - quantity=int(Decimal(str(quantity)) * Decimal(1e18)), - price=int(Decimal(str(price)) * Decimal(1e18)), - isBuy=side.value, - orderType=order_type.value, - timeInForce=time_in_force.value, - nonce=nonce, - expiration=(ts + 1000 * 60 * 60 * 24) * 1000, - **self.get_shared_params(), - ) - response = await self.send_message_to_endpoint("/v1/order", "POST", message) - return response + return await super().create_order(*args, **kwargs) - async def cancel_order(self, product_id: int, order_id: int): + async def cancel_order(self, *args, **kwargs): """ Cancel an order. """ - message = self.generate_and_sign_message( - CancelOrder, - subAccountId=self.subaccount_id, - productId=product_id, - orderId=order_id, - **self.get_shared_params(), - ) - response = await self.send_message_to_endpoint("/v1/order", "DELETE", message) - return response + return await super().cancel_order(*args, **kwargs) - async def cancel_and_replace_order( - self, - subaccount_id: int, - product_id: int, - quantity: int, - price: int, - side: OrderSide, - order_id_to_cancel: str, - nonce: int = 0, - ): + async def cancel_and_replace_order(self, *args, **kwargs): """ Cancel and replace an order. """ - ts = self._current_timestamp() - if nonce == 0: - nonce = ts - _message = self.generate_and_sign_message( - Order, - subAccountId=subaccount_id, - productId=product_id, - quantity=int(Decimal(str(quantity)) * Decimal(1e18)), - price=int(Decimal(str(price)) * Decimal(1e18)), - isBuy=side.value, - orderType=OrderType.LIMIT_MAKER.value, - timeInForce=TimeInForce.GTC.value, - nonce=nonce, - expiration=(ts + 1000 * 60 * 60 * 24) * 1000, - **self.get_shared_params(), - ) - message = {} - message["newOrder"] = from_message_to_payload(_message) - message["idToCancel"] = order_id_to_cancel - response = await self.send_message_to_endpoint("/v1/order/cancel-and-replace", "POST", message) - return response - + return await super().cancel_and_replace_order(*args, **kwargs) + async def cancel_all_orders(self, subaccount_id: int, product_id: int): """ Cancel all orders. """ - message = self.generate_and_sign_message( - CancelOrders, - subAccountId=subaccount_id, - productId=product_id, - **self.get_shared_params(), - ) - response = await self.send_message_to_endpoint("/v1/openOrders", "DELETE", message) - return response - + return await super().cancel_all_orders(subaccount_id, product_id) + async def login(self): """ Login to the exchange. @@ -193,29 +92,31 @@ async def login(self): if response is None: raise Exception("Failed to login") - async def send_message_to_endpoint(self, endpoint: str, method: str, message: dict = {}, authenticated: bool = True): + async def send_message_to_endpoint( + self, endpoint: str, method: str, message: dict = {}, authenticated: bool = True, params: dict = {} + ): """ Send a message to an endpoint. """ + if not self._validate_function( + endpoint, + ): + raise ClientError(f"Invalid endpoint: {endpoint}") payload = from_message_to_payload(message) + async with httpx.AsyncClient() as client: - if method.upper() == "GET": - response = await client.request( - method, - self.rest_url + endpoint, - headers={} if not authenticated else self.authenticated_headers, - params=payload, - ) - else: - response = await client.request( - method, - self.rest_url + endpoint, - headers={} if not authenticated else self.authenticated_headers, - json=payload, - ) - + response = await client.request( + method, + self.rest_url + endpoint, + params=params, + headers={} if not authenticated else self.authenticated_headers, + json=payload, + ) + if response.status_code != 200: - raise Exception(f"Failed to send message: {response.text} {response.status_code} {self.rest_url} {payload}") + raise Exception( + f"Failed to send message: {response.text} {response.status_code} {self.rest_url} {payload}" + ) return response.json() async def create_authenticated_session_with_service(self): @@ -227,4 +128,4 @@ async def create_authenticated_session_with_service(self): ) response = await self.send_message_to_endpoint("/v1/session/login", "POST", login_payload, authenticated=False) self.session_cookie = response.get("value") - return response \ No newline at end of file + return response diff --git a/hundred_x/client.py b/hundred_x/client.py index ab6ed9b..e6ba254 100644 --- a/hundred_x/client.py +++ b/hundred_x/client.py @@ -48,6 +48,7 @@ class HundredXClient: public_functions: List[str] = [ "/v1/products", "/v1/products/{product_symbol}", + "/v1/ticker/24hr?symbol={symbol}", "/v1/trade-history", "/v1/time", "/v1/uiKlines", @@ -55,6 +56,10 @@ class HundredXClient: "/v1/depth", ] + @property + def http_client(self): + return requests + def __init__( self, env: Environment = Environment.TESTNET, @@ -87,7 +92,6 @@ def __init__( chainId=CONTRACTS[env]["CHAIN_ID"], verifyingContract=CONTRACTS[env]["VERIFYING_CONTRACT"], ) - self.set_referral_code() def _validate_function( self, @@ -133,7 +137,9 @@ def get_shared_params(self, asset: str = None, subaccount_id: int = None): params["subAccountId"] = subaccount_id return params - def send_message_to_endpoint(self, endpoint: str, method: str, message: dict, authenticated: bool = True): + def send_message_to_endpoint( + self, endpoint: str, method: str, message: dict = {}, authenticated: bool = True, params=None + ): """ Send a message to an endpoint. """ @@ -142,9 +148,10 @@ def send_message_to_endpoint(self, endpoint: str, method: str, message: dict, au ): raise ClientError(f"Invalid endpoint: {endpoint}") payload = from_message_to_payload(message) - response = requests.request( + response = self.http_client.request( method, self.rest_url + endpoint, + params=params, headers={} if not authenticated else self.authenticated_headers, json=payload, ) @@ -199,13 +206,15 @@ def create_order( def cancel_and_replace_order( self, - subaccount_id: int, product_id: int, quantity: int, price: int, side: OrderSide, order_id_to_cancel: str, nonce: int = 0, + subaccount_id: int = None, + order_type: OrderType = OrderType.LIMIT_MAKER, + time_in_force: TimeInForce = TimeInForce.GTC, ): """ Cancel and replace an order. @@ -213,6 +222,8 @@ def cancel_and_replace_order( ts = self._current_timestamp() if nonce == 0: nonce = ts + if subaccount_id is None: + subaccount_id = self.subaccount_id _message = self.generate_and_sign_message( Order, subAccountId=subaccount_id, @@ -220,8 +231,8 @@ def cancel_and_replace_order( quantity=int(Decimal(str(quantity)) * Decimal(1e18)), price=int(Decimal(str(price)) * Decimal(1e18)), isBuy=side.value, - orderType=OrderType.LIMIT_MAKER.value, - timeInForce=TimeInForce.GTC.value, + orderType=order_type.value, + timeInForce=time_in_force.value, nonce=nonce, expiration=(ts + 1000 * 60 * 60 * 24) * 1000, **self.get_shared_params(), @@ -231,13 +242,13 @@ def cancel_and_replace_order( message["idToCancel"] = order_id_to_cancel return self.send_message_to_endpoint("/v1/order/cancel-and-replace", "POST", message) - def cancel_order(self, subaccount_id: int, product_id: int, order_id: int): + def cancel_order(self, product_id: int, order_id: int, subaccount_id: int = None): """ Cancel an order. """ message = self.generate_and_sign_message( CancelOrder, - subAccountId=subaccount_id, + subAccountId=self.subaccount_id if subaccount_id is None else subaccount_id, productId=product_id, orderId=order_id, **self.get_shared_params(), @@ -264,6 +275,10 @@ def create_authenticated_session_with_service(self): **self.get_shared_params(), ) response = self.send_message_to_endpoint("/v1/session/login", "POST", login_payload, authenticated=False) + try: + self.set_referral_code() + except Exception: # pylint: disable=broad-except + pass self.session_cookie = response.get("value") return response @@ -283,10 +298,11 @@ def get_trade_history(self, symbol: str, lookback: int) -> Any: """ Get the trade history for a specific product symbol and lookback amount. """ - return requests.get( - self.rest_url + "/v1/trade-history", + return self.send_message_to_endpoint( + endpoint="/v1/trade-history", + method="GET", params={"symbol": symbol, "lookback": lookback}, - ).json() + ) def get_server_time(self) -> Any: """ @@ -308,12 +324,23 @@ def get_candlestick(self, symbol: str, **kwargs) -> Any: params=params, ).json() - def get_symbol(self, symbol: str) -> Any: + def get_symbol(self, symbol: str = None) -> Any: """ Get the details of a specific symbol. + If symbol is None, return all symbols. """ - endpoint = f"/v1/ticker/24hr?symbol={symbol}" - return requests.get(self.rest_url + endpoint).json()[0] + response = self.send_message_to_endpoint( + endpoint="/v1/ticker/24hr", + method="GET", + message={}, + params={"symbol": symbol} if symbol else {}, + ) + if not isinstance(response, list): + return response + if symbol: + return response[0] + else: + return response def get_depth(self, symbol: str, **kwargs) -> Any: """ @@ -324,10 +351,11 @@ def get_depth(self, symbol: str, **kwargs) -> Any: var = kwargs.get(arg) if var is not None: params[arg] = var - return requests.get( - self.rest_url + "/v1/depth", + return self.send_message_to_endpoint( + endpoint="/v1/depth", + method="GET", params=params, - ).json() + ) def login(self): """ @@ -359,24 +387,21 @@ def get_spot_balances(self): """ Get the spot balances. """ - return requests.get( - self.rest_url + "/v1/balances", - headers=self.authenticated_headers, + return self.send_message_to_endpoint( + "/v1/balances", + "GET", params={"account": self.public_key, "subAccountId": self.subaccount_id}, - ).json() + authenticated=True, + ) - def get_position(self): + def get_position(self, symbol: str = None): """ Get all positions for the subaccount. """ - return requests.get( - self.rest_url + "/v1/positionRisk", - headers=self.authenticated_headers, - params={ - "account": self.public_key, - "subAccountId": self.subaccount_id, - }, - ).json() + params = {"account": self.public_key, "subAccountId": self.subaccount_id} + if symbol is not None: + params["symbol"] = symbol + return self.send_message_to_endpoint("/v1/positionRisk", "GET", params=params, authenticated=True) def get_approved_signers(self): """ @@ -398,11 +423,7 @@ def get_open_orders( params = {"account": self.public_key, "subAccountId": self.subaccount_id} if symbol is not None: params["symbol"] = symbol - return requests.get( - self.rest_url + "/v1/openOrders", - headers=self.authenticated_headers, - params=params, - ).json() + return self.send_message_to_endpoint("/v1/openOrders", "GET", params=params, authenticated=True) def get_orders(self, symbol: str = None, ids: List[str] = None): """ diff --git a/poetry.lock b/poetry.lock index 954548d..b97803d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,9 +1,10 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry and should not be changed by hand. [[package]] name = "aiohttp" version = "3.9.5" description = "Async http client/server framework (asyncio)" +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -100,6 +101,7 @@ speedups = ["Brotli", "aiodns", "brotlicffi"] name = "aiosignal" version = "1.3.1" description = "aiosignal: a list of registered asynchronous callbacks" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -110,10 +112,34 @@ files = [ [package.dependencies] frozenlist = ">=1.1.0" +[[package]] +name = "anyio" +version = "4.4.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +category = "main" +optional = false +python-versions = ">=3.8" +files = [ + {file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"}, + {file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"}, +] + +[package.dependencies] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} +idna = ">=2.8" +sniffio = ">=1.1" +typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} + +[package.extras] +doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +trio = ["trio (>=0.23)"] + [[package]] name = "astroid" version = "3.2.2" description = "An abstract syntax tree for Python with inference support." +category = "dev" optional = false python-versions = ">=3.8.0" files = [ @@ -128,6 +154,7 @@ typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} name = "async-timeout" version = "4.0.3" description = "Timeout context manager for asyncio programs" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -139,6 +166,7 @@ files = [ name = "attrs" version = "23.2.0" description = "Classes Without Boilerplate" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -158,6 +186,7 @@ tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "p name = "bitarray" version = "2.9.2" description = "efficient arrays of booleans -- C extension" +category = "main" optional = false python-versions = "*" files = [ @@ -289,6 +318,7 @@ files = [ name = "black" version = "24.4.2" description = "The uncompromising code formatter." +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -335,6 +365,7 @@ uvloop = ["uvloop (>=0.15.2)"] name = "certifi" version = "2024.2.2" description = "Python package for providing Mozilla's CA Bundle." +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -346,6 +377,7 @@ files = [ name = "charset-normalizer" version = "3.3.2" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" optional = false python-versions = ">=3.7.0" files = [ @@ -445,6 +477,7 @@ files = [ name = "cli-ui" version = "0.17.2" description = "Build Nice User Interfaces In The Terminal" +category = "dev" optional = false python-versions = ">=3.7,<4.0" files = [ @@ -461,6 +494,7 @@ unidecode = ">=1.0.23,<2.0.0" name = "click" version = "8.1.7" description = "Composable command line interface toolkit" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -475,6 +509,7 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." +category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ @@ -486,6 +521,7 @@ files = [ name = "cytoolz" version = "0.12.3" description = "Cython implementation of Toolz: High performance functional utilities" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -605,6 +641,7 @@ cython = ["cython"] name = "dill" version = "0.3.8" description = "serialize all of Python" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -620,6 +657,7 @@ profile = ["gprof2dot (>=2022.7.29)"] name = "docopt" version = "0.6.2" description = "Pythonic argument parser, that will make you smile" +category = "dev" optional = false python-versions = "*" files = [ @@ -630,6 +668,7 @@ files = [ name = "eip712-structs-ng" version = "2.0.1" description = "A Python interface for EIP-712 struct construction." +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -648,6 +687,7 @@ tests = ["eth-tester (==0.10.0b1)", "mypy", "py-evm (==0.8.0b1)", "py-solc-x (== name = "eth-abi" version = "5.1.0" description = "eth_abi: Python utilities for working with Ethereum ABI definitions, especially encoding and decoding" +category = "main" optional = false python-versions = "<4,>=3.8" files = [ @@ -670,6 +710,7 @@ tools = ["hypothesis (>=4.18.2,<5.0.0)"] name = "eth-account" version = "0.11.0" description = "eth-account: Sign Ethereum transactions and messages with local private keys" +category = "main" optional = false python-versions = ">=3.8, <4" files = [ @@ -696,6 +737,7 @@ test = ["coverage", "hypothesis (>=4.18.0,<5)", "pytest (>=7.0.0)", "pytest-xdis name = "eth-hash" version = "0.7.0" description = "eth-hash: The Ethereum hashing function, keccak256, sometimes (erroneously) called sha3" +category = "main" optional = false python-versions = ">=3.8, <4" files = [ @@ -717,6 +759,7 @@ test = ["pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] name = "eth-keyfile" version = "0.8.1" description = "eth-keyfile: A library for handling the encrypted keyfiles used to store ethereum private keys" +category = "main" optional = false python-versions = "<4,>=3.8" files = [ @@ -738,6 +781,7 @@ test = ["pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] name = "eth-keys" version = "0.5.1" description = "eth-keys: Common API for Ethereum key operations" +category = "main" optional = false python-versions = "<4,>=3.8" files = [ @@ -759,6 +803,7 @@ test = ["asn1tools (>=0.146.2)", "eth-hash[pysha3]", "factory-boy (>=3.0.1)", "h name = "eth-rlp" version = "1.0.1" description = "eth-rlp: RLP definitions for common Ethereum objects in Python" +category = "main" optional = false python-versions = ">=3.8, <4" files = [ @@ -781,6 +826,7 @@ test = ["eth-hash[pycryptodome]", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] name = "eth-typing" version = "4.2.3" description = "eth-typing: Common type annotations for ethereum python packages" +category = "main" optional = false python-versions = "<4,>=3.8" files = [ @@ -797,6 +843,7 @@ test = ["pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] name = "eth-utils" version = "2.3.1" description = "eth-utils: Common utility functions for python code that interacts with Ethereum" +category = "main" optional = false python-versions = ">=3.7,<4" files = [ @@ -820,6 +867,7 @@ test = ["hypothesis (>=4.43.0)", "mypy (==0.971)", "pytest (>=7.0.0)", "pytest-x name = "exceptiongroup" version = "1.2.1" description = "Backport of PEP 654 (exception groups)" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -834,6 +882,7 @@ test = ["pytest (>=6)"] name = "flake8" version = "7.0.0" description = "the modular source code checker: pep8 pyflakes and co" +category = "dev" optional = false python-versions = ">=3.8.1" files = [ @@ -850,6 +899,7 @@ pyflakes = ">=3.2.0,<3.3.0" name = "frozenlist" version = "1.4.1" description = "A list-like structure which implements collections.abc.MutableSequence" +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -932,10 +982,23 @@ files = [ {file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"}, ] +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + [[package]] name = "hexbytes" version = "0.3.1" description = "hexbytes: Python `bytes` subclass that decodes hex, with a readable console output" +category = "main" optional = false python-versions = ">=3.7, <4" files = [ @@ -949,10 +1012,58 @@ doc = ["sphinx (>=5.0.0)", "sphinx-rtd-theme (>=1.0.0)", "towncrier (>=21,<22)"] lint = ["black (>=22)", "flake8 (==6.0.0)", "flake8-bugbear (==23.3.23)", "isort (>=5.10.1)", "mypy (==0.971)", "pydocstyle (>=5.0.0)"] test = ["eth-utils (>=1.0.1,<3)", "hypothesis (>=3.44.24,<=6.31.6)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] +[[package]] +name = "httpcore" +version = "1.0.5" +description = "A minimal low-level HTTP client." +category = "main" +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"}, + {file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.13,<0.15" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (>=1.0.0,<2.0.0)"] +trio = ["trio (>=0.22.0,<0.26.0)"] + +[[package]] +name = "httpx" +version = "0.27.0" +description = "The next generation HTTP client." +category = "main" +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"}, + {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +httpcore = ">=1.0.0,<2.0.0" +idna = "*" +sniffio = "*" + +[package.extras] +brotli = ["brotli", "brotlicffi"] +cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (>=1.0.0,<2.0.0)"] + [[package]] name = "idna" version = "3.7" description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -964,6 +1075,7 @@ files = [ name = "iniconfig" version = "2.0.0" description = "brain-dead simple config-ini parsing" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -975,6 +1087,7 @@ files = [ name = "isort" version = "5.13.2" description = "A Python utility / library to sort Python imports." +category = "dev" optional = false python-versions = ">=3.8.0" files = [ @@ -989,6 +1102,7 @@ colors = ["colorama (>=0.4.6)"] name = "jsonschema" version = "4.22.0" description = "An implementation of JSON Schema validation for Python" +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1010,6 +1124,7 @@ format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339- name = "jsonschema-specifications" version = "2023.12.1" description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1024,6 +1139,7 @@ referencing = ">=0.31.0" name = "lru-dict" version = "1.2.0" description = "An Dict like LRU container." +category = "main" optional = false python-versions = "*" files = [ @@ -1118,6 +1234,7 @@ test = ["pytest"] name = "mccabe" version = "0.7.0" description = "McCabe checker, plugin for flake8" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1129,6 +1246,7 @@ files = [ name = "multidict" version = "6.0.5" description = "multidict implementation" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1228,6 +1346,7 @@ files = [ name = "mypy-extensions" version = "1.0.0" description = "Type system extensions for programs checked with the mypy type checker." +category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -1239,6 +1358,7 @@ files = [ name = "packaging" version = "24.0" description = "Core utilities for Python packages" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1250,6 +1370,7 @@ files = [ name = "parsimonious" version = "0.10.0" description = "(Soon to be) the fastest pure-Python PEG parser I could muster" +category = "main" optional = false python-versions = "*" files = [ @@ -1264,6 +1385,7 @@ regex = ">=2022.3.15" name = "pathspec" version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1275,6 +1397,7 @@ files = [ name = "platformdirs" version = "4.2.2" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1291,6 +1414,7 @@ type = ["mypy (>=1.8)"] name = "pluggy" version = "1.5.0" description = "plugin and hook calling mechanisms for python" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1306,6 +1430,7 @@ testing = ["pytest", "pytest-benchmark"] name = "protobuf" version = "5.27.0" description = "" +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1326,6 +1451,7 @@ files = [ name = "pycodestyle" version = "2.11.1" description = "Python style guide checker" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1337,6 +1463,7 @@ files = [ name = "pycryptodome" version = "3.20.0" description = "Cryptographic library for Python" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -1378,6 +1505,7 @@ files = [ name = "pyflakes" version = "3.2.0" description = "passive checker of Python programs" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1389,6 +1517,7 @@ files = [ name = "pylint" version = "3.2.2" description = "python code static checker" +category = "dev" optional = false python-versions = ">=3.8.0" files = [ @@ -1401,8 +1530,8 @@ astroid = ">=3.2.2,<=3.3.0-dev0" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} dill = [ {version = ">=0.2", markers = "python_version < \"3.11\""}, + {version = ">=0.3.6", markers = "python_version >= \"3.11\""}, {version = ">=0.3.7", markers = "python_version >= \"3.12\""}, - {version = ">=0.3.6", markers = "python_version >= \"3.11\" and python_version < \"3.12\""}, ] isort = ">=4.2.5,<5.13.0 || >5.13.0,<6" mccabe = ">=0.6,<0.8" @@ -1419,6 +1548,7 @@ testutils = ["gitpython (>3)"] name = "pytest" version = "8.2.1" description = "pytest: simple powerful testing with Python" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1437,10 +1567,30 @@ tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +[[package]] +name = "pytest-asyncio" +version = "0.23.7" +description = "Pytest support for asyncio" +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest_asyncio-0.23.7-py3-none-any.whl", hash = "sha256:009b48127fbe44518a547bddd25611551b0e43ccdbf1e67d12479f569832c20b"}, + {file = "pytest_asyncio-0.23.7.tar.gz", hash = "sha256:5f5c72948f4c49e7db4f29f2521d4031f1c27f86e57b046126654083d4770268"}, +] + +[package.dependencies] +pytest = ">=7.0.0,<9" + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] +testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] + [[package]] name = "pyunormalize" version = "15.1.0" description = "Unicode normalization forms (NFC, NFKC, NFD, NFKD). A library independent from the Python core Unicode database." +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1451,6 +1601,7 @@ files = [ name = "pywin32" version = "306" description = "Python for Window Extensions" +category = "main" optional = false python-versions = "*" files = [ @@ -1474,6 +1625,7 @@ files = [ name = "referencing" version = "0.35.1" description = "JSON Referencing + Python" +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1489,6 +1641,7 @@ rpds-py = ">=0.7.0" name = "regex" version = "2024.5.15" description = "Alternative regular expression module, to replace re." +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1577,6 +1730,7 @@ files = [ name = "requests" version = "2.32.3" description = "Python HTTP for Humans." +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1594,10 +1748,26 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "respx" +version = "0.21.1" +description = "A utility for mocking out the Python HTTPX and HTTP Core libraries." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "respx-0.21.1-py2.py3-none-any.whl", hash = "sha256:05f45de23f0c785862a2c92a3e173916e8ca88e4caad715dd5f68584d6053c20"}, + {file = "respx-0.21.1.tar.gz", hash = "sha256:0bd7fe21bfaa52106caa1223ce61224cf30786985f17c63c5d71eff0307ee8af"}, +] + +[package.dependencies] +httpx = ">=0.21.0" + [[package]] name = "rlp" version = "4.0.1" description = "rlp: A package for Recursive Length Prefix encoding and decoding" +category = "main" optional = false python-versions = "<4,>=3.8" files = [ @@ -1618,6 +1788,7 @@ test = ["hypothesis (==5.19.0)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] name = "rpds-py" version = "0.18.1" description = "Python bindings to Rust's persistent data structures (rpds)" +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1726,6 +1897,7 @@ files = [ name = "safe-pysha3" version = "1.0.4" description = "SHA-3 (Keccak) for Python 3.9 - 3.11" +category = "main" optional = false python-versions = "*" files = [ @@ -1736,6 +1908,7 @@ files = [ name = "schema" version = "0.7.7" description = "Simple data validation library" +category = "dev" optional = false python-versions = "*" files = [ @@ -1747,6 +1920,7 @@ files = [ name = "semver" version = "3.0.2" description = "Python helper for Semantic Versioning (https://semver.org)" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1754,10 +1928,23 @@ files = [ {file = "semver-3.0.2.tar.gz", hash = "sha256:6253adb39c70f6e51afed2fa7152bcd414c411286088fb4b9effb133885ab4cc"}, ] +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + [[package]] name = "tabulate" version = "0.8.10" description = "Pretty-print tabular data" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -1772,6 +1959,7 @@ widechars = ["wcwidth"] name = "tbump" version = "6.11.0" description = "Bump software releases" +category = "dev" optional = false python-versions = ">=3.7,<4.0" files = [ @@ -1789,6 +1977,7 @@ tomlkit = ">=0.11,<0.12" name = "tomli" version = "2.0.1" description = "A lil' TOML parser" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1800,6 +1989,7 @@ files = [ name = "tomlkit" version = "0.11.8" description = "Style preserving TOML library" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1811,6 +2001,7 @@ files = [ name = "toolz" version = "0.12.1" description = "List processing tools and functional utilities" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1822,6 +2013,7 @@ files = [ name = "typing-extensions" version = "4.12.0" description = "Backported and Experimental Type Hints for Python 3.8+" +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1833,6 +2025,7 @@ files = [ name = "unidecode" version = "1.3.8" description = "ASCII transliterations of Unicode text" +category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -1844,6 +2037,7 @@ files = [ name = "urllib3" version = "2.2.1" description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1861,6 +2055,7 @@ zstd = ["zstandard (>=0.18.0)"] name = "web3" version = "6.19.0" description = "web3.py" +category = "main" optional = false python-versions = ">=3.7.2" files = [ @@ -1895,6 +2090,7 @@ tester = ["eth-tester[py-evm] (>=0.11.0b1,<0.12.0b1)", "eth-tester[py-evm] (>=0. name = "websockets" version = "12.0" description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1976,6 +2172,7 @@ files = [ name = "yarl" version = "1.9.4" description = "Yet another URL library" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2078,4 +2275,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = ">=3.9,<=3.13" -content-hash = "b4e9c3c2ae6ffbb84991de263d27e603485843eaaf0bae9477b2954154ab410f" +content-hash = "5958ab1d4d9f0c0ced1fb24a3d2cf52a48460c4a7787fc335321cec8d4ad67f1" diff --git a/pyproject.toml b/pyproject.toml index e99965d..a7e3228 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,8 +15,6 @@ web3 = ">=6,<7" websockets = ">=9" safe-pysha3 = "^1.0.4" httpx = "^0.27.0" -pytest-asyncio = "^0.23.7" -respx = "^0.21.1" [tool.poetry.group.dev.dependencies] @@ -27,6 +25,8 @@ black = "^24.3.0" flake8 = "^7.0.0" semver = "^3.0.2" tbump = "^6.11.0" +pytest-asyncio = "^0.23.7" +respx = "^0.21.1" [build-system] requires = ["poetry-core"] @@ -68,4 +68,4 @@ exclude = ''' markers = [ "devs: test devnet (deselect with '-m dev')", ] -addopts = "-m 'not dev'" \ No newline at end of file +addopts = "-m 'not dev' -p no:warnings" \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_async_hundred_x_client.py b/tests/test_async_hundred_x_client.py index 7ebf776..a1f191c 100644 --- a/tests/test_async_hundred_x_client.py +++ b/tests/test_async_hundred_x_client.py @@ -1,92 +1,94 @@ +""" +Tests for the async client. +""" + import pytest import respx from httpx import Response -from decimal import Decimal -from hundred_x.enums import OrderSide, OrderType, TimeInForce + from hundred_x.async_client import AsyncHundredXClient from hundred_x.enums import Environment +from tests.test_data import TEST_ORDER_ID, TEST_PRICE, TEST_PRIVATE_KEY, TEST_SYMBOL -TEST_PRIVATE_KEY = "0x8f58e47491ac5fe6897216208fe1fed316d6ee89de6c901bfc521c2178ebe6dd" @pytest.fixture def client(): env = Environment.PROD return AsyncHundredXClient(env, TEST_PRIVATE_KEY) -BASE_URL = "https://api.100x.finance" @pytest.mark.asyncio @respx.mock -async def test_get_symbol_info(client): - symbol = "BTCUSD" - respx.get(f"{BASE_URL}/v1/ticker/24hr", params={"symbol": symbol}).mock( - return_value=Response(200, json=[{"symbol": symbol, "price": "50000.00"}]) +async def test_get_symbol( + client, +): + respx.get(f"{client.rest_url}/v1/ticker/24hr", params={"symbol": TEST_SYMBOL}).mock( + return_value=Response(200, json=[{"productSymbol": TEST_SYMBOL}]) ) - response = await client.get_symbol_info(symbol) - assert response["symbol"] == symbol - assert response["price"] == "50000.00" + response = await client.get_symbol(TEST_SYMBOL) + assert response["productSymbol"] == TEST_SYMBOL + @pytest.mark.asyncio -@respx.mock async def test_get_depth(client): - symbol = "BTCUSD" - respx.get(f"{BASE_URL}/v1/depth", params={"symbol": symbol, "limit": 5}).mock( + respx.get(f"{client.rest_url}/v1/depth", params={"symbol": TEST_SYMBOL, "limit": 5}).mock( return_value=Response(200, json={"bids": [], "asks": []}) ) - response = await client.get_depth(symbol) + response = await client.get_depth(TEST_SYMBOL) assert "bids" in response assert "asks" in response + @pytest.mark.asyncio @respx.mock async def test_get_trade_history(client): - symbol = "BTCUSD" - lookback = 10 - respx.get(f"{BASE_URL}/v1/trade-history", params={"symbol": symbol, "limit": lookback}).mock( - return_value=Response(200, json=[{"id": 1, "price": "50000.00"}]) - ) - response = await client.get_trade_history(symbol, lookback) - assert isinstance(response, list) - assert response[0]["id"] == 1 - assert response[0]["price"] == "50000.00" + respx.get( + f"{client.rest_url}/v1/trade-history", + params={"symbol": TEST_SYMBOL}, + ).mock(return_value=Response(200, json={"success": True, "trades": [{"id": TEST_ORDER_ID, "price": TEST_PRICE}]})) + response = await client.get_trade_history(TEST_SYMBOL) + assert isinstance(response['trades'], list) + assert response['trades'][0]["id"] == TEST_ORDER_ID + assert response['trades'][0]["price"] == TEST_PRICE + @pytest.mark.asyncio @respx.mock -async def test_create_and_send_order(client): - subaccount_id = 1 - product_id = 1 - quantity = 1 - price = 50000 - side = OrderSide.BUY - order_type = OrderType.LIMIT - time_in_force = TimeInForce.GTC - respx.post(f"{BASE_URL}/v1/order").mock( - return_value=Response(200, json={"orderId": "12345"}) - ) - response = await client.create_and_send_order( - subaccount_id, product_id, quantity, price, side, order_type, time_in_force - ) - assert response["orderId"] == "12345" +async def test_get_position(client): + respx.get( + f"{client.rest_url}/v1/positionRisk", + ).mock(return_value=Response(200, json={"symbol": TEST_SYMBOL})) + response = await client.get_position(TEST_SYMBOL) + assert response["symbol"] == TEST_SYMBOL + @pytest.mark.asyncio @respx.mock -async def test_cancel_order(client): - product_id = 1 - order_id = "12345" # Changing order_id to string - respx.delete(f"{BASE_URL}/v1/order").mock( - return_value=Response(200, json={"status": "success"}) - ) - response = await client.cancel_order(product_id, order_id) - assert response["status"] == "success" +async def test_get_spot_balances(client): + respx.get( + f"{client.rest_url}/v1/balances", params={"account": client.public_key, "subAccountId": client.subaccount_id} + ).mock(return_value=Response(200, json={"balances": []})) + response = await client.get_spot_balances() + assert "balances" in response + @pytest.mark.asyncio @respx.mock async def test_get_open_orders(client): - symbol = "BTCUSD" - respx.get(f"{BASE_URL}/v1/openOrders", params={"symbol": symbol, "account": client.public_key, "subAccountId": client.subaccount_id}).mock( - return_value=Response(200, json=[{"orderId": "12345", "symbol": symbol}]) + respx.get(f"{client.rest_url}/v1/openOrders", params={"symbol": TEST_SYMBOL}).mock( + return_value=Response(200, json=[{"orderId": "12345", "symbol": TEST_SYMBOL}]) ) - response = await client.get_open_orders(symbol) + response = await client.get_open_orders(TEST_SYMBOL) assert isinstance(response, list) assert response[0]["orderId"] == "12345" - assert response[0]["symbol"] == symbol \ No newline at end of file + assert response[0]["symbol"] == TEST_SYMBOL + + +@pytest.mark.asyncio +@respx.mock +async def test_cancel_order(client): + respx.delete( + f"{client.rest_url}/v1/order", + ).mock(return_value=Response(200, json={"orderId": TEST_ORDER_ID})) + response = await client.cancel_order(order_id=TEST_ORDER_ID, product_id=1002) + assert response["orderId"] == TEST_ORDER_ID diff --git a/tests/test_data.py b/tests/test_data.py index 09f104e..b35a998 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -8,6 +8,9 @@ DEFAULT_SYMBOL = "ethperp" TEST_PRIVATE_KEY = "0x8f58e47491ac5fe6897216208fe1fed316d6ee89de6c901bfc521c2178ebe6dd" TEST_ADDRESS = "0xEEF7faba495b4875d67E3ED8FB3a32433d3DB3b3" +TEST_SYMBOL = "btcperp" +TEST_PRICE = "50000.00" +TEST_ORDER_ID = "12345" TEST_ORDER = { "subaccount_id": 1, "product_id": 1002,