diff --git a/CHANGELOG.md b/CHANGELOG.md index b2aa44d..e05652b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,22 @@ # Changelog +## [1.5.0] - 2024-AUG-21 + +### Added +- `get_all_products` parameter to `get_products` and `get_public_products` +- `aggregation_price_increment` parameter to `get_product_book` and `get_public_product_book` +- Support for API key permissions endpoint with`get_api_key_permissions` +- Support for auto generating unique client_order_id when set to empty string +- Support for Futures Balance Summary Channel in `WSClient` + +### Changed +- Heartbeats channel methods no longer require `product_ids` + ## [1.4.3] - 2024-JUL-22 ### Added -- - `order_ids`, `time_in_forces` and `sort_by` parameters in List Orders - - `trade_ids` and `sort_by` in List Fills +- `order_ids`, `time_in_forces` and `sort_by` parameters in List Orders +- `trade_ids` and `sort_by` in List Fills ### Changed - `skip_fcm_risk_check` parameter removed from various Orders methods. diff --git a/README.md b/README.md index 0d5dd9f..929748f 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,9 @@ print(dumps(order, indent=2)) ``` This code calls the `get_accounts` and `market_order_buy` endpoints. +TIP: Setting `client_order_id` to the empty string will auto generate a unique client_order_id per call. +However, this will remove the intended safeguard of accidentally placing duplicate orders. + Refer to the [Advanced API Reference](https://docs.cdp.coinbase.com/advanced-trade/reference) for detailed information on each exposed endpoint. Look in the `coinbase.rest` module to see the API hooks that are exposed. diff --git a/coinbase/__version__.py b/coinbase/__version__.py index aa56ed4..5b60188 100644 --- a/coinbase/__version__.py +++ b/coinbase/__version__.py @@ -1 +1 @@ -__version__ = "1.4.3" +__version__ = "1.5.0" diff --git a/coinbase/constants.py b/coinbase/constants.py index 5e6f242..a2d99e4 100644 --- a/coinbase/constants.py +++ b/coinbase/constants.py @@ -7,11 +7,9 @@ # REST Constants BASE_URL = "api.coinbase.com" API_PREFIX = "/api/v3/brokerage" -REST_SERVICE = "retail_rest_api_proxy" # Websocket Constants WS_BASE_URL = "wss://advanced-trade-ws.coinbase.com" -WS_SERVICE = "public_websocket_api" WS_RETRY_MAX = 5 WS_RETRY_BASE = 5 @@ -30,5 +28,6 @@ TICKER_BATCH = "ticker_batch" LEVEL2 = "level2" USER = "user" +FUTURES_BALANCE_SUMMARY = "futures_balance_summary" -WS_AUTH_CHANNELS = {USER} +WS_AUTH_CHANNELS = {USER, FUTURES_BALANCE_SUMMARY} diff --git a/coinbase/jwt_generator.py b/coinbase/jwt_generator.py index b31f2a4..528c0a1 100644 --- a/coinbase/jwt_generator.py +++ b/coinbase/jwt_generator.py @@ -4,7 +4,7 @@ import jwt from cryptography.hazmat.primitives import serialization -from coinbase.constants import BASE_URL, REST_SERVICE, WS_SERVICE +from coinbase.constants import BASE_URL def build_jwt(key_var, secret_var, uri=None) -> str: diff --git a/coinbase/rest/__init__.py b/coinbase/rest/__init__.py index 44bf028..bb0ffc6 100755 --- a/coinbase/rest/__init__.py +++ b/coinbase/rest/__init__.py @@ -2,12 +2,32 @@ class RESTClient(RESTBase): + """ + **RESTClient** + _____________________________ + + Initialize using RESTClient + + __________ + + **Parameters**: + + - **api_key | Optional (str)** - The API key + - **api_secret | Optional (str)** - The API key secret + - **key_file | Optional (IO | str)** - Path to API key file or file-like object + - **base_url | (str)** - The base URL for REST requests. Default set to "https://api.coinbase.com" + - **timeout | Optional (int)** - Set timeout in seconds for REST requests + - **verbose | Optional (bool)** - Enables debug logging. Default set to False + + + """ + from .accounts import get_account, get_accounts from .convert import commit_convert_trade, create_convert_quote, get_convert_trade + from .data_api import get_api_key_permissions from .fees import get_transaction_summary from .futures import ( cancel_pending_futures_sweep, - close_position, get_current_margin_window, get_futures_balance_summary, get_futures_position, @@ -20,6 +40,7 @@ class RESTClient(RESTBase): from .market_data import get_candles, get_market_trades from .orders import ( cancel_orders, + close_position, create_order, edit_order, get_fills, diff --git a/coinbase/rest/data_api.py b/coinbase/rest/data_api.py new file mode 100644 index 0000000..f669503 --- /dev/null +++ b/coinbase/rest/data_api.py @@ -0,0 +1,28 @@ +from typing import Any, Dict, Optional + +from coinbase.constants import API_PREFIX + + +def get_api_key_permissions( + self, + **kwargs, +) -> Dict[str, Any]: + """ + **Get Api Key Permissions** + _____________________________ + + [GET] https://api.coinbase.com/api/v3/brokerage/key_permissions + + __________ + + **Description:** + + Get information about your CDP API key permissions + + __________ + + **Read more on the official documentation:** `Create Convert Quote `_ + """ + endpoint = f"{API_PREFIX}/key_permissions" + + return self.get(endpoint, **kwargs) diff --git a/coinbase/rest/futures.py b/coinbase/rest/futures.py index 456c7c1..f74f15c 100644 --- a/coinbase/rest/futures.py +++ b/coinbase/rest/futures.py @@ -3,32 +3,6 @@ from coinbase.constants import API_PREFIX -def close_position( - self, client_order_id: str, product_id: str, size: Optional[str] = None, **kwargs -) -> Dict[str, Any]: - """ - **Close Position** - _________________ - - [POST] https://api.coinbase.com/api/v3/brokerage/orders/close_position - - __________ - - **Description:** - - Places an order to close any open positions for a specified ``product_id``. - - __________ - - **Read more on the official documentation:** `Close Position - `_ - """ - endpoint = f"{API_PREFIX}/orders/close_position" - data = {"client_order_id": client_order_id, "product_id": product_id, "size": size} - - return self.post(endpoint, data=data, **kwargs) - - def get_futures_balance_summary(self, **kwargs) -> Dict[str, Any]: """ **Get Futures Balance Summary** diff --git a/coinbase/rest/market_data.py b/coinbase/rest/market_data.py index e415e68..319680b 100644 --- a/coinbase/rest/market_data.py +++ b/coinbase/rest/market_data.py @@ -4,7 +4,13 @@ def get_candles( - self, product_id: str, start: str, end: str, granularity: str, **kwargs + self, + product_id: str, + start: str, + end: str, + granularity: str, + limit: Optional[int] = None, + **kwargs, ) -> Dict[str, Any]: """ **Get Product Candles** @@ -25,11 +31,7 @@ def get_candles( """ endpoint = f"{API_PREFIX}/products/{product_id}/candles" - params = { - "start": start, - "end": end, - "granularity": granularity, - } + params = {"start": start, "end": end, "granularity": granularity, "limit": limit} return self.get(endpoint, params=params, **kwargs) diff --git a/coinbase/rest/orders.py b/coinbase/rest/orders.py index df7afec..b9e2f57 100644 --- a/coinbase/rest/orders.py +++ b/coinbase/rest/orders.py @@ -1,8 +1,16 @@ +import uuid from typing import Any, Dict, List, Optional from coinbase.constants import API_PREFIX +def generate_client_order_id() -> str: + """ + :meta private: + """ + return uuid.uuid4().hex + + def create_order( self, client_order_id: str, @@ -34,6 +42,9 @@ def create_order( """ endpoint = f"{API_PREFIX}/orders" + if not client_order_id: + client_order_id = generate_client_order_id() + data = { "client_order_id": client_order_id, "product_id": product_id, @@ -1626,9 +1637,6 @@ def preview_order( product_id: str, side: str, order_configuration, - commission_rate: Optional[str] = None, - is_max: Optional[bool] = False, - tradable_balance: Optional[str] = None, leverage: Optional[str] = None, margin_type: Optional[str] = None, retail_portfolio_id: Optional[str] = None, @@ -1653,16 +1661,10 @@ def preview_order( """ endpoint = f"{API_PREFIX}/orders/preview" - if commission_rate: - commission_rate = {"value": commission_rate} - data = { "product_id": product_id, "side": side, "order_configuration": order_configuration, - "commission_rate": commission_rate, - "is_max": is_max, - "tradable_balance": tradable_balance, "leverage": leverage, "margin_type": margin_type, "retail_portfolio_id": retail_portfolio_id, @@ -1678,9 +1680,6 @@ def preview_market_order( side: str, quote_size: Optional[str] = None, base_size: Optional[str] = None, - commission_rate: Optional[str] = None, - is_max: Optional[bool] = False, - tradable_balance: Optional[str] = None, leverage: Optional[str] = None, margin_type: Optional[str] = None, retail_portfolio_id: Optional[str] = None, @@ -1716,9 +1715,6 @@ def preview_market_order( product_id, side, order_configuration, - commission_rate=commission_rate, - is_max=is_max, - tradable_balance=tradable_balance, leverage=leverage, margin_type=margin_type, retail_portfolio_id=retail_portfolio_id, @@ -1731,9 +1727,6 @@ def preview_market_order_buy( product_id: str, quote_size: Optional[str] = None, base_size: Optional[str] = None, - commission_rate: Optional[str] = None, - is_max: Optional[bool] = False, - tradable_balance: Optional[str] = None, leverage: Optional[str] = None, margin_type: Optional[str] = None, retail_portfolio_id: Optional[str] = None, @@ -1762,9 +1755,6 @@ def preview_market_order_buy( "BUY", quote_size=quote_size, base_size=base_size, - commission_rate=commission_rate, - is_max=is_max, - tradable_balance=tradable_balance, leverage=leverage, margin_type=margin_type, retail_portfolio_id=retail_portfolio_id, @@ -1776,9 +1766,6 @@ def preview_market_order_sell( self, product_id: str, base_size: str, - commission_rate: Optional[str] = None, - is_max: Optional[bool] = False, - tradable_balance: Optional[str] = None, leverage: Optional[str] = None, margin_type: Optional[str] = None, retail_portfolio_id: Optional[str] = None, @@ -1806,9 +1793,6 @@ def preview_market_order_sell( product_id, "SELL", base_size=base_size, - commission_rate=commission_rate, - is_max=is_max, - tradable_balance=tradable_balance, leverage=leverage, margin_type=margin_type, retail_portfolio_id=retail_portfolio_id, @@ -1823,9 +1807,6 @@ def preview_limit_order_ioc( side: str, base_size: str, limit_price: str, - commission_rate: Optional[str] = None, - is_max: Optional[bool] = False, - tradable_balance: Optional[str] = None, leverage: Optional[str] = None, margin_type: Optional[str] = None, retail_portfolio_id: Optional[str] = None, @@ -1857,9 +1838,6 @@ def preview_limit_order_ioc( product_id, side, order_configuration, - commission_rate=commission_rate, - is_max=is_max, - tradable_balance=tradable_balance, leverage=leverage, margin_type=margin_type, retail_portfolio_id=retail_portfolio_id, @@ -1872,9 +1850,6 @@ def preview_limit_order_ioc_buy( product_id: str, base_size: str, limit_price: str, - commission_rate: Optional[str] = None, - is_max: Optional[bool] = False, - tradable_balance: Optional[str] = None, leverage: Optional[str] = None, margin_type: Optional[str] = None, retail_portfolio_id: Optional[str] = None, @@ -1903,9 +1878,6 @@ def preview_limit_order_ioc_buy( "BUY", base_size=base_size, limit_price=limit_price, - commission_rate=commission_rate, - is_max=is_max, - tradable_balance=tradable_balance, leverage=leverage, margin_type=margin_type, retail_portfolio_id=retail_portfolio_id, @@ -1918,9 +1890,6 @@ def preview_limit_order_ioc_sell( product_id: str, base_size: str, limit_price: str, - commission_rate: Optional[str] = None, - is_max: Optional[bool] = False, - tradable_balance: Optional[str] = None, leverage: Optional[str] = None, margin_type: Optional[str] = None, retail_portfolio_id: Optional[str] = None, @@ -1949,9 +1918,6 @@ def preview_limit_order_ioc_sell( "SELL", base_size=base_size, limit_price=limit_price, - commission_rate=commission_rate, - is_max=is_max, - tradable_balance=tradable_balance, leverage=leverage, margin_type=margin_type, retail_portfolio_id=retail_portfolio_id, @@ -1967,9 +1933,6 @@ def preview_limit_order_gtc( base_size: str, limit_price: str, post_only: bool = False, - commission_rate: Optional[str] = None, - is_max: Optional[bool] = False, - tradable_balance: Optional[str] = None, leverage: Optional[str] = None, margin_type: Optional[str] = None, retail_portfolio_id: Optional[str] = None, @@ -2005,9 +1968,6 @@ def preview_limit_order_gtc( product_id, side, order_configuration, - commission_rate=commission_rate, - is_max=is_max, - tradable_balance=tradable_balance, leverage=leverage, margin_type=margin_type, retail_portfolio_id=retail_portfolio_id, @@ -2021,9 +1981,6 @@ def preview_limit_order_gtc_buy( base_size: str, limit_price: str, post_only: bool = False, - commission_rate: Optional[str] = None, - is_max: Optional[bool] = False, - tradable_balance: Optional[str] = None, leverage: Optional[str] = None, margin_type: Optional[str] = None, retail_portfolio_id: Optional[str] = None, @@ -2053,9 +2010,6 @@ def preview_limit_order_gtc_buy( base_size=base_size, limit_price=limit_price, post_only=post_only, - commission_rate=commission_rate, - is_max=is_max, - tradable_balance=tradable_balance, leverage=leverage, margin_type=margin_type, retail_portfolio_id=retail_portfolio_id, @@ -2069,9 +2023,6 @@ def preview_limit_order_gtc_sell( base_size: str, limit_price: str, post_only: bool = False, - commission_rate: Optional[str] = None, - is_max: Optional[bool] = False, - tradable_balance: Optional[str] = None, leverage: Optional[str] = None, margin_type: Optional[str] = None, retail_portfolio_id: Optional[str] = None, @@ -2101,9 +2052,6 @@ def preview_limit_order_gtc_sell( base_size=base_size, limit_price=limit_price, post_only=post_only, - commission_rate=commission_rate, - is_max=is_max, - tradable_balance=tradable_balance, leverage=leverage, margin_type=margin_type, retail_portfolio_id=retail_portfolio_id, @@ -2120,9 +2068,6 @@ def preview_limit_order_gtd( limit_price: str, end_time: str, post_only: bool = False, - commission_rate: Optional[str] = None, - is_max: Optional[bool] = False, - tradable_balance: Optional[str] = None, leverage: Optional[str] = None, margin_type: Optional[str] = None, retail_portfolio_id: Optional[str] = None, @@ -2159,9 +2104,6 @@ def preview_limit_order_gtd( product_id, side, order_configuration, - commission_rate=commission_rate, - is_max=is_max, - tradable_balance=tradable_balance, leverage=leverage, margin_type=margin_type, retail_portfolio_id=retail_portfolio_id, @@ -2176,9 +2118,6 @@ def preview_limit_order_gtd_buy( limit_price: str, end_time: str, post_only: bool = False, - commission_rate: Optional[str] = None, - is_max: Optional[bool] = False, - tradable_balance: Optional[str] = None, leverage: Optional[str] = None, margin_type: Optional[str] = None, retail_portfolio_id: Optional[str] = None, @@ -2209,9 +2148,6 @@ def preview_limit_order_gtd_buy( limit_price=limit_price, end_time=end_time, post_only=post_only, - commission_rate=commission_rate, - is_max=is_max, - tradable_balance=tradable_balance, leverage=leverage, margin_type=margin_type, retail_portfolio_id=retail_portfolio_id, @@ -2226,9 +2162,6 @@ def preview_limit_order_gtd_sell( limit_price: str, end_time: str, post_only: bool = False, - commission_rate: Optional[str] = None, - is_max: Optional[bool] = False, - tradable_balance: Optional[str] = None, leverage: Optional[str] = None, margin_type: Optional[str] = None, retail_portfolio_id: Optional[str] = None, @@ -2259,9 +2192,6 @@ def preview_limit_order_gtd_sell( limit_price=limit_price, end_time=end_time, post_only=post_only, - commission_rate=commission_rate, - is_max=is_max, - tradable_balance=tradable_balance, leverage=leverage, margin_type=margin_type, retail_portfolio_id=retail_portfolio_id, @@ -2275,9 +2205,6 @@ def preview_limit_order_fok( side: str, base_size: str, limit_price: str, - commission_rate: Optional[str] = None, - is_max: Optional[bool] = False, - tradable_balance: Optional[str] = None, leverage: Optional[str] = None, margin_type: Optional[str] = None, retail_portfolio_id: Optional[str] = None, @@ -2309,9 +2236,6 @@ def preview_limit_order_fok( product_id, side, order_configuration, - commission_rate=commission_rate, - is_max=is_max, - tradable_balance=tradable_balance, leverage=leverage, margin_type=margin_type, retail_portfolio_id=retail_portfolio_id, @@ -2324,9 +2248,6 @@ def preview_limit_order_fok_buy( product_id: str, base_size: str, limit_price: str, - commission_rate: Optional[str] = None, - is_max: Optional[bool] = False, - tradable_balance: Optional[str] = None, leverage: Optional[str] = None, margin_type: Optional[str] = None, retail_portfolio_id: Optional[str] = None, @@ -2355,9 +2276,6 @@ def preview_limit_order_fok_buy( "BUY", base_size=base_size, limit_price=limit_price, - commission_rate=commission_rate, - is_max=is_max, - tradable_balance=tradable_balance, leverage=leverage, margin_type=margin_type, retail_portfolio_id=retail_portfolio_id, @@ -2370,9 +2288,6 @@ def preview_limit_order_fok_sell( product_id: str, base_size: str, limit_price: str, - commission_rate: Optional[str] = None, - is_max: Optional[bool] = False, - tradable_balance: Optional[str] = None, leverage: Optional[str] = None, margin_type: Optional[str] = None, retail_portfolio_id: Optional[str] = None, @@ -2401,9 +2316,6 @@ def preview_limit_order_fok_sell( "SELL", base_size=base_size, limit_price=limit_price, - commission_rate=commission_rate, - is_max=is_max, - tradable_balance=tradable_balance, leverage=leverage, margin_type=margin_type, retail_portfolio_id=retail_portfolio_id, @@ -2420,9 +2332,6 @@ def preview_stop_limit_order_gtc( limit_price: str, stop_price: str, stop_direction: str, - commission_rate: Optional[str] = None, - is_max: Optional[bool] = False, - tradable_balance: Optional[str] = None, leverage: Optional[str] = None, margin_type: Optional[str] = None, retail_portfolio_id: Optional[str] = None, @@ -2459,9 +2368,6 @@ def preview_stop_limit_order_gtc( product_id, side, order_configuration, - commission_rate=commission_rate, - is_max=is_max, - tradable_balance=tradable_balance, leverage=leverage, margin_type=margin_type, retail_portfolio_id=retail_portfolio_id, @@ -2476,9 +2382,6 @@ def preview_stop_limit_order_gtc_buy( limit_price: str, stop_price: str, stop_direction: str, - commission_rate: Optional[str] = None, - is_max: Optional[bool] = False, - tradable_balance: Optional[str] = None, leverage: Optional[str] = None, margin_type: Optional[str] = None, retail_portfolio_id: Optional[str] = None, @@ -2509,9 +2412,6 @@ def preview_stop_limit_order_gtc_buy( limit_price=limit_price, stop_price=stop_price, stop_direction=stop_direction, - commission_rate=commission_rate, - is_max=is_max, - tradable_balance=tradable_balance, leverage=leverage, margin_type=margin_type, retail_portfolio_id=retail_portfolio_id, @@ -2526,9 +2426,6 @@ def preview_stop_limit_order_gtc_sell( limit_price: str, stop_price: str, stop_direction: str, - commission_rate: Optional[str] = None, - is_max: Optional[bool] = False, - tradable_balance: Optional[str] = None, leverage: Optional[str] = None, margin_type: Optional[str] = None, retail_portfolio_id: Optional[str] = None, @@ -2559,9 +2456,6 @@ def preview_stop_limit_order_gtc_sell( limit_price=limit_price, stop_price=stop_price, stop_direction=stop_direction, - commission_rate=commission_rate, - is_max=is_max, - tradable_balance=tradable_balance, leverage=leverage, margin_type=margin_type, retail_portfolio_id=retail_portfolio_id, @@ -2579,9 +2473,6 @@ def preview_stop_limit_order_gtd( stop_price: str, end_time: str, stop_direction: str, - commission_rate: Optional[str] = None, - is_max: Optional[bool] = False, - tradable_balance: Optional[str] = None, leverage: Optional[str] = None, margin_type: Optional[str] = None, retail_portfolio_id: Optional[str] = None, @@ -2619,9 +2510,6 @@ def preview_stop_limit_order_gtd( product_id, side, order_configuration, - commission_rate=commission_rate, - is_max=is_max, - tradable_balance=tradable_balance, leverage=leverage, margin_type=margin_type, retail_portfolio_id=retail_portfolio_id, @@ -2637,9 +2525,6 @@ def preview_stop_limit_order_gtd_buy( stop_price: str, end_time: str, stop_direction: str, - commission_rate: Optional[str] = None, - is_max: Optional[bool] = False, - tradable_balance: Optional[str] = None, leverage: Optional[str] = None, margin_type: Optional[str] = None, retail_portfolio_id: Optional[str] = None, @@ -2671,9 +2556,6 @@ def preview_stop_limit_order_gtd_buy( stop_price=stop_price, end_time=end_time, stop_direction=stop_direction, - commission_rate=commission_rate, - is_max=is_max, - tradable_balance=tradable_balance, leverage=leverage, margin_type=margin_type, retail_portfolio_id=retail_portfolio_id, @@ -2689,9 +2571,6 @@ def preview_stop_limit_order_gtd_sell( stop_price: str, end_time: str, stop_direction: str, - commission_rate: Optional[str] = None, - is_max: Optional[bool] = False, - tradable_balance: Optional[str] = None, leverage: Optional[str] = None, margin_type: Optional[str] = None, retail_portfolio_id: Optional[str] = None, @@ -2723,9 +2602,6 @@ def preview_stop_limit_order_gtd_sell( stop_price=stop_price, end_time=end_time, stop_direction=stop_direction, - commission_rate=commission_rate, - is_max=is_max, - tradable_balance=tradable_balance, leverage=leverage, margin_type=margin_type, retail_portfolio_id=retail_portfolio_id, @@ -2741,9 +2617,6 @@ def preview_trigger_bracket_order_gtc( base_size: str, limit_price: str, stop_trigger_price: str, - commission_rate: Optional[str] = None, - is_max: Optional[bool] = False, - tradable_balance: Optional[str] = None, leverage: Optional[str] = None, margin_type: Optional[str] = None, retail_portfolio_id: Optional[str] = None, @@ -2779,9 +2652,6 @@ def preview_trigger_bracket_order_gtc( product_id, side, order_configuration, - commission_rate=commission_rate, - is_max=is_max, - tradable_balance=tradable_balance, leverage=leverage, margin_type=margin_type, retail_portfolio_id=retail_portfolio_id, @@ -2795,9 +2665,6 @@ def preview_trigger_bracket_order_gtc_buy( base_size: str, limit_price: str, stop_trigger_price: str, - commission_rate: Optional[str] = None, - is_max: Optional[bool] = False, - tradable_balance: Optional[str] = None, leverage: Optional[str] = None, margin_type: Optional[str] = None, retail_portfolio_id: Optional[str] = None, @@ -2827,9 +2694,6 @@ def preview_trigger_bracket_order_gtc_buy( base_size=base_size, limit_price=limit_price, stop_trigger_price=stop_trigger_price, - commission_rate=commission_rate, - is_max=is_max, - tradable_balance=tradable_balance, leverage=leverage, margin_type=margin_type, retail_portfolio_id=retail_portfolio_id, @@ -2843,9 +2707,6 @@ def preview_trigger_bracket_order_gtc_sell( base_size: str, limit_price: str, stop_trigger_price: str, - commission_rate: Optional[str] = None, - is_max: Optional[bool] = False, - tradable_balance: Optional[str] = None, leverage: Optional[str] = None, margin_type: Optional[str] = None, retail_portfolio_id: Optional[str] = None, @@ -2875,9 +2736,6 @@ def preview_trigger_bracket_order_gtc_sell( base_size=base_size, limit_price=limit_price, stop_trigger_price=stop_trigger_price, - commission_rate=commission_rate, - is_max=is_max, - tradable_balance=tradable_balance, leverage=leverage, margin_type=margin_type, retail_portfolio_id=retail_portfolio_id, @@ -2894,9 +2752,6 @@ def preview_trigger_bracket_order_gtd( limit_price: str, stop_trigger_price: str, end_time: str, - commission_rate: Optional[str] = None, - is_max: Optional[bool] = False, - tradable_balance: Optional[str] = None, leverage: Optional[str] = None, margin_type: Optional[str] = None, retail_portfolio_id: Optional[str] = None, @@ -2933,9 +2788,6 @@ def preview_trigger_bracket_order_gtd( product_id, side, order_configuration, - commission_rate=commission_rate, - is_max=is_max, - tradable_balance=tradable_balance, leverage=leverage, margin_type=margin_type, retail_portfolio_id=retail_portfolio_id, @@ -2950,9 +2802,6 @@ def preview_trigger_bracket_order_gtd_buy( limit_price: str, stop_trigger_price: str, end_time: str, - commission_rate: Optional[str] = None, - is_max: Optional[bool] = False, - tradable_balance: Optional[str] = None, leverage: Optional[str] = None, margin_type: Optional[str] = None, retail_portfolio_id: Optional[str] = None, @@ -2983,9 +2832,6 @@ def preview_trigger_bracket_order_gtd_buy( limit_price=limit_price, stop_trigger_price=stop_trigger_price, end_time=end_time, - commission_rate=commission_rate, - is_max=is_max, - tradable_balance=tradable_balance, leverage=leverage, margin_type=margin_type, retail_portfolio_id=retail_portfolio_id, @@ -3000,9 +2846,6 @@ def preview_trigger_bracket_order_gtd_sell( limit_price: str, stop_trigger_price: str, end_time: str, - commission_rate: Optional[str] = None, - is_max: Optional[bool] = False, - tradable_balance: Optional[str] = None, leverage: Optional[str] = None, margin_type: Optional[str] = None, retail_portfolio_id: Optional[str] = None, @@ -3033,11 +2876,38 @@ def preview_trigger_bracket_order_gtd_sell( limit_price=limit_price, stop_trigger_price=stop_trigger_price, end_time=end_time, - commission_rate=commission_rate, - is_max=is_max, - tradable_balance=tradable_balance, leverage=leverage, margin_type=margin_type, retail_portfolio_id=retail_portfolio_id, **kwargs, ) + + +def close_position( + self, client_order_id: str, product_id: str, size: Optional[str] = None, **kwargs +) -> Dict[str, Any]: + """ + **Close Position** + _________________ + + [POST] https://api.coinbase.com/api/v3/brokerage/orders/close_position + + __________ + + **Description:** + + Places an order to close any open positions for a specified ``product_id``. + + __________ + + **Read more on the official documentation:** `Close Position + `_ + """ + endpoint = f"{API_PREFIX}/orders/close_position" + + if not client_order_id: + client_order_id = generate_client_order_id() + + data = {"client_order_id": client_order_id, "product_id": product_id, "size": size} + + return self.post(endpoint, data=data, **kwargs) diff --git a/coinbase/rest/portfolios.py b/coinbase/rest/portfolios.py index 17e3e56..e53f734 100644 --- a/coinbase/rest/portfolios.py +++ b/coinbase/rest/portfolios.py @@ -57,7 +57,9 @@ def create_portfolio(self, name: str, **kwargs) -> Dict[str, Any]: return self.post(endpoint, data=data, **kwargs) -def get_portfolio_breakdown(self, portfolio_uuid: str, **kwargs) -> Dict[str, Any]: +def get_portfolio_breakdown( + self, portfolio_uuid: str, currency: Optional[str] = None, **kwargs +) -> Dict[str, Any]: """ **Get Portfolio Breakdown** ___________________________ @@ -77,7 +79,8 @@ def get_portfolio_breakdown(self, portfolio_uuid: str, **kwargs) -> Dict[str, An """ endpoint = f"{API_PREFIX}/portfolios/{portfolio_uuid}" - return self.get(endpoint, **kwargs) + params = {"currency": currency} + return self.get(endpoint, params=params, **kwargs) def move_portfolio_funds( diff --git a/coinbase/rest/products.py b/coinbase/rest/products.py index be73a84..b2a43d8 100644 --- a/coinbase/rest/products.py +++ b/coinbase/rest/products.py @@ -12,6 +12,7 @@ def get_products( contract_expiry_type: Optional[str] = None, expiring_contract_status: Optional[str] = None, get_tradability_status: Optional[bool] = False, + get_all_products: Optional[bool] = False, **kwargs, ) -> Dict[str, Any]: """ @@ -41,6 +42,7 @@ def get_products( "contract_expiry_type": contract_expiry_type, "expiring_contract_status": expiring_contract_status, "get_tradability_status": get_tradability_status, + "get_all_products": get_all_products, } return self.get(endpoint, params=params, **kwargs) @@ -76,7 +78,11 @@ def get_product( def get_product_book( - self, product_id: str, limit: Optional[int] = None, **kwargs + self, + product_id: str, + limit: Optional[int] = None, + aggregation_price_increment: Optional[str] = None, + **kwargs, ) -> Dict[str, Any]: """ **Get Product Book** @@ -97,7 +103,11 @@ def get_product_book( """ endpoint = f"{API_PREFIX}/product_book" - params = {"product_id": product_id, "limit": limit} + params = { + "product_id": product_id, + "limit": limit, + "aggregation_price_increment": aggregation_price_increment, + } return self.get(endpoint, params=params, **kwargs) diff --git a/coinbase/rest/public.py b/coinbase/rest/public.py index 5f1cced..ab8f9c4 100644 --- a/coinbase/rest/public.py +++ b/coinbase/rest/public.py @@ -26,7 +26,11 @@ def get_unix_time(self, **kwargs) -> Dict[str, Any]: def get_public_product_book( - self, product_id: str, limit: Optional[int] = None, **kwargs + self, + product_id: str, + limit: Optional[int] = None, + aggregation_price_increment: Optional[str] = None, + **kwargs, ) -> Dict[str, Any]: """ **Get Public Product Book** @@ -52,7 +56,11 @@ def get_public_product_book( endpoint = f"{API_PREFIX}/market/product_book" - params = {"product_id": product_id, "limit": limit} + params = { + "product_id": product_id, + "limit": limit, + "aggregation_price_increment": aggregation_price_increment, + } return self.get(endpoint, params=params, public=True, **kwargs) @@ -65,6 +73,7 @@ def get_public_products( product_ids: Optional[List[str]] = None, contract_expiry_type: Optional[str] = None, expiring_contract_status: Optional[str] = None, + get_all_products: bool = False, **kwargs, ) -> Dict[str, Any]: """ @@ -99,6 +108,7 @@ def get_public_products( "product_ids": product_ids, "contract_expiry_type": contract_expiry_type, "expiring_contract_status": expiring_contract_status, + "get_all_products": get_all_products, } return self.get(endpoint, params=params, public=True, **kwargs) @@ -134,7 +144,13 @@ def get_public_product(self, product_id: str, **kwargs) -> Dict[str, Any]: def get_public_candles( - self, product_id: str, start: str, end: str, granularity: str, **kwargs + self, + product_id: str, + start: str, + end: str, + granularity: str, + limit: Optional[int] = None, + **kwargs, ) -> Dict[str, Any]: """ **Get Public Product Candles** @@ -161,11 +177,7 @@ def get_public_candles( """ endpoint = f"{API_PREFIX}/market/products/{product_id}/candles" - params = { - "start": start, - "end": end, - "granularity": granularity, - } + params = {"start": start, "end": end, "granularity": granularity, "limit": limit} return self.get(endpoint, params=params, public=True, **kwargs) diff --git a/coinbase/rest/rest_base.py b/coinbase/rest/rest_base.py index 9863aed..e06b3ab 100644 --- a/coinbase/rest/rest_base.py +++ b/coinbase/rest/rest_base.py @@ -43,23 +43,7 @@ def handle_exception(response): class RESTBase(APIBase): """ - **RESTClient** - _____________________________ - - Initialize using RESTClient - - __________ - - **Parameters**: - - - **api_key | Optional (str)** - The API key - - **api_secret | Optional (str)** - The API key secret - - **key_file | Optional (IO | str)** - Path to API key file or file-like object - - **base_url | (str)** - The base URL for REST requests. Default set to "https://api.coinbase.com" - - **timeout | Optional (int)** - Set timeout in seconds for REST requests - - **verbose | Optional (bool)** - Enables debug logging. Default set to False - - + :meta private: """ def __init__( diff --git a/coinbase/websocket/__init__.py b/coinbase/websocket/__init__.py index 0bbeebf..d717a94 100644 --- a/coinbase/websocket/__init__.py +++ b/coinbase/websocket/__init__.py @@ -2,11 +2,40 @@ class WSClient(WSBase): + """ + **WSClient** + _____________________________ + + Initialize using WSClient + + __________ + + **Parameters**: + + - **api_key | Optional (str)** - The API key + - **api_secret | Optional (str)** - The API key secret + - **key_file | Optional (IO | str)** - Path to API key file or file-like object + - **base_url | (str)** - The websocket base url. Default set to "wss://advanced-trade-ws.coinbase.com" + - **timeout | Optional (int)** - Set timeout in seconds for REST requests + - **max_size | Optional (int)** - Max size in bytes for messages received. Default set to (10 * 1024 * 1024) + - **on_message | Optional (Callable[[str], None])** - Function called when a message is received + - **on_open | Optional ([Callable[[], None]])** - Function called when a connection is opened + - **on_close | Optional ([Callable[[], None]])** - Function called when a connection is closed + - **retry | Optional (bool)** - Enables automatic reconnections. Default set to True + - **verbose | Optional (bool)** - Enables debug logging. Default set to False + + + """ + from .channels import ( candles, candles_async, candles_unsubscribe, candles_unsubscribe_async, + futures_balance_summary, + futures_balance_summary_async, + futures_balance_summary_unsubscribe, + futures_balance_summary_unsubscribe_async, heartbeats, heartbeats_async, heartbeats_unsubscribe, diff --git a/coinbase/websocket/channels.py b/coinbase/websocket/channels.py index 4fcd73a..9b7b058 100644 --- a/coinbase/websocket/channels.py +++ b/coinbase/websocket/channels.py @@ -2,6 +2,7 @@ from coinbase.constants import ( CANDLES, + FUTURES_BALANCE_SUMMARY, HEARTBEATS, LEVEL2, MARKET_TRADES, @@ -12,7 +13,7 @@ ) -def heartbeats(self, product_ids: List[str]) -> None: +def heartbeats(self) -> None: """ **Heartbeats Subscribe** ________________________ @@ -21,17 +22,17 @@ def heartbeats(self, product_ids: List[str]) -> None: **Description:** - Subscribe to heartbeats channel for a list of products_ids. + Subscribe to heartbeats channel. __________ **Read more on the official documentation:** `Heartbeats Channel `_ """ - self.subscribe(product_ids, [HEARTBEATS]) + self.subscribe([], [HEARTBEATS]) -async def heartbeats_async(self, product_ids: List[str]) -> None: +async def heartbeats_async(self) -> None: """ **Heartbeats Subscribe Async** ______________________________ @@ -40,17 +41,17 @@ async def heartbeats_async(self, product_ids: List[str]) -> None: **Description:** - Async subscribe to heartbeats channel for a list of products_ids. + Async subscribe to heartbeats channel. __________ **Read more on the official documentation:** `Heartbeats Channel `_ """ - await self.subscribe_async(product_ids, [HEARTBEATS]) + await self.subscribe_async([], [HEARTBEATS]) -def heartbeats_unsubscribe(self, product_ids: List[str]) -> None: +def heartbeats_unsubscribe(self) -> None: """ **Heartbeats Unsubscribe** __________________________ @@ -59,17 +60,19 @@ def heartbeats_unsubscribe(self, product_ids: List[str]) -> None: **Description:** - Unsubscribe to heartbeats channel for a list of products_ids. + Unsubscribe to heartbeats channel. __________ **Read more on the official documentation:** `Heartbeats Channel `_ """ - self.unsubscribe(product_ids, [HEARTBEATS]) + self.unsubscribe([], [HEARTBEATS]) -async def heartbeats_unsubscribe_async(self, product_ids: List[str]) -> None: +async def heartbeats_unsubscribe_async( + self, +) -> None: """ **Heartbeats Unsubscribe Async** ________________________________ @@ -78,14 +81,14 @@ async def heartbeats_unsubscribe_async(self, product_ids: List[str]) -> None: **Description:** - Async unsubscribe to heartbeats channel for a list of products_ids. + Async unsubscribe to heartbeats channel. __________ **Read more on the official documentation:** `Heartbeats Channel `_ """ - await self.unsubscribe_async(product_ids, [HEARTBEATS]) + await self.unsubscribe_async([], [HEARTBEATS]) def candles(self, product_ids: List[str]) -> None: @@ -618,3 +621,79 @@ async def user_unsubscribe_async(self, product_ids: List[str]) -> None: `_ """ await self.unsubscribe_async(product_ids, [USER]) + + +def futures_balance_summary(self) -> None: + """ + **Futures Balance Summary Subscribe** + __________________ + + __________ + + **Description:** + + Subscribe to futures_balance_summary channel. + + __________ + + **Read more on the official documentation:** `Futures Balance Summary Channel + `_ + """ + self.subscribe([], [FUTURES_BALANCE_SUMMARY]) + + +async def futures_balance_summary_async(self) -> None: + """ + **Futures Balance Summary Subscribe Async** + ________________________ + + __________ + + **Description:** + + Async subscribe to futures_balance_summary channel. + + __________ + + **Read more on the official documentation:** `Futures Balance Summary Channel + `_ + """ + await self.subscribe_async([], [FUTURES_BALANCE_SUMMARY]) + + +def futures_balance_summary_unsubscribe(self) -> None: + """ + **Futures Balance Summary Unsubscribe** + ____________________ + + __________ + + **Description:** + + Unsubscribe to futures_balance_summary channel. + + __________ + + **Read more on the official documentation:** `Futures Balance Summary Channel + `_ + """ + self.unsubscribe([], [FUTURES_BALANCE_SUMMARY]) + + +async def futures_balance_summary_unsubscribe_async(self) -> None: + """ + **Futures Balance Summary Unsubscribe Async** + __________________________ + + __________ + + **Description:** + + Async unsubscribe to futures_balance_summary channel. + + __________ + + **Read more on the official documentation:** `Futures Balance Summary Channel + `_ + """ + await self.unsubscribe_async([], [FUTURES_BALANCE_SUMMARY]) diff --git a/coinbase/websocket/websocket_base.py b/coinbase/websocket/websocket_base.py index 1d5de34..a65b8f4 100644 --- a/coinbase/websocket/websocket_base.py +++ b/coinbase/websocket/websocket_base.py @@ -57,28 +57,7 @@ class WSClientConnectionClosedException(Exception): class WSBase(APIBase): """ - **WSBase Client** - _____________________________ - - Initialize using WSClient - - __________ - - **Parameters**: - - - **api_key | Optional (str)** - The API key - - **api_secret | Optional (str)** - The API key secret - - **key_file | Optional (IO | str)** - Path to API key file or file-like object - - **base_url | (str)** - The websocket base url. Default set to "wss://advanced-trade-ws.coinbase.com" - - **timeout | Optional (int)** - Set timeout in seconds for REST requests - - **max_size | Optional (int)** - Max size in bytes for messages received. Default set to (10 * 1024 * 1024) - - **on_message | Optional (Callable[[str], None])** - Function called when a message is received - - **on_open | Optional ([Callable[[], None]])** - Function called when a connection is opened - - **on_close | Optional ([Callable[[], None]])** - Function called when a connection is closed - - **retry | Optional (bool)** - Enables automatic reconnections. Default set to True - - **verbose | Optional (bool)** - Enables debug logging. Default set to False - - + :meta private: """ def __init__( @@ -118,6 +97,7 @@ def __init__( self.websocket = None self.loop = None self.thread = None + self._task = None self.retry = retry self._retry_max_tries = WS_RETRY_MAX @@ -176,7 +156,8 @@ async def open_async(self) -> None: # Start the message handler coroutine after establishing connection if not self._retrying: - asyncio.create_task(self._message_handler()) + self._task = asyncio.create_task(self._message_handler()) + except asyncio.TimeoutError as toe: self.websocket = None logger.error("Connection attempt timed out: %s", toe) diff --git a/docs/coinbase.rest.rst b/docs/coinbase.rest.rst index a7a6fe2..c2c7941 100644 --- a/docs/coinbase.rest.rst +++ b/docs/coinbase.rest.rst @@ -5,103 +5,165 @@ REST API Client RESTClient Constructor ------------------------------- -.. autofunction:: coinbase.rest.rest_base.RESTBase +.. autoclass:: coinbase.rest.RESTClient REST Utils ------------------------------- -.. autofunction:: coinbase.rest.rest_base.RESTBase.get - -.. autofunction:: coinbase.rest.rest_base.RESTBase.post - -.. autofunction:: coinbase.rest.rest_base.RESTBase.put - -.. autofunction:: coinbase.rest.rest_base.RESTBase.delete +.. autofunction:: coinbase.rest.RESTClient.get +.. autofunction:: coinbase.rest.RESTClient.post +.. autofunction:: coinbase.rest.RESTClient.put +.. autofunction:: coinbase.rest.RESTClient.delete Accounts ----------------------------- -.. automodule:: coinbase.rest.accounts - :members: - :undoc-members: - :show-inheritance: +.. autofunction:: coinbase.rest.RESTClient.get_accounts +.. autofunction:: coinbase.rest.RESTClient.get_account Products ----------------------------- -.. automodule:: coinbase.rest.products - :members: - :undoc-members: - :show-inheritance: +.. autofunction:: coinbase.rest.RESTClient.get_products +.. autofunction:: coinbase.rest.RESTClient.get_product +.. autofunction:: coinbase.rest.RESTClient.get_product_book +.. autofunction:: coinbase.rest.RESTClient.get_best_bid_ask Market Data --------------------------------- -.. automodule:: coinbase.rest.market_data - :members: - :undoc-members: - :show-inheritance: +.. autofunction:: coinbase.rest.RESTClient.get_candles +.. autofunction:: coinbase.rest.RESTClient.get_market_trades Orders --------------------------- -.. automodule:: coinbase.rest.orders - :members: - :undoc-members: - :show-inheritance: +.. autofunction:: coinbase.rest.RESTClient.create_order +.. autofunction:: coinbase.rest.RESTClient.market_order +.. autofunction:: coinbase.rest.RESTClient.market_order_buy +.. autofunction:: coinbase.rest.RESTClient.market_order_sell +.. autofunction:: coinbase.rest.RESTClient.limit_order_ioc +.. autofunction:: coinbase.rest.RESTClient.limit_order_ioc_buy +.. autofunction:: coinbase.rest.RESTClient.limit_order_ioc_sell +.. autofunction:: coinbase.rest.RESTClient.limit_order_gtc +.. autofunction:: coinbase.rest.RESTClient.limit_order_gtc_buy +.. autofunction:: coinbase.rest.RESTClient.limit_order_gtc_sell +.. autofunction:: coinbase.rest.RESTClient.limit_order_gtd +.. autofunction:: coinbase.rest.RESTClient.limit_order_gtd_buy +.. autofunction:: coinbase.rest.RESTClient.limit_order_gtd_sell +.. autofunction:: coinbase.rest.RESTClient.limit_order_fok +.. autofunction:: coinbase.rest.RESTClient.limit_order_fok_buy +.. autofunction:: coinbase.rest.RESTClient.limit_order_fok_sell +.. autofunction:: coinbase.rest.RESTClient.stop_limit_order_gtc +.. autofunction:: coinbase.rest.RESTClient.stop_limit_order_gtc_buy +.. autofunction:: coinbase.rest.RESTClient.stop_limit_order_gtc_sell +.. autofunction:: coinbase.rest.RESTClient.stop_limit_order_gtd +.. autofunction:: coinbase.rest.RESTClient.stop_limit_order_gtd_buy +.. autofunction:: coinbase.rest.RESTClient.stop_limit_order_gtd_sell +.. autofunction:: coinbase.rest.RESTClient.trigger_bracket_order_gtc +.. autofunction:: coinbase.rest.RESTClient.trigger_bracket_order_gtc_buy +.. autofunction:: coinbase.rest.RESTClient.trigger_bracket_order_gtc_sell +.. autofunction:: coinbase.rest.RESTClient.trigger_bracket_order_gtd +.. autofunction:: coinbase.rest.RESTClient.trigger_bracket_order_gtd_buy +.. autofunction:: coinbase.rest.RESTClient.trigger_bracket_order_gtd_sell +.. autofunction:: coinbase.rest.RESTClient.get_order +.. autofunction:: coinbase.rest.RESTClient.list_orders +.. autofunction:: coinbase.rest.RESTClient.get_fills +.. autofunction:: coinbase.rest.RESTClient.edit_order +.. autofunction:: coinbase.rest.RESTClient.preview_edit_order +.. autofunction:: coinbase.rest.RESTClient.cancel_orders +.. autofunction:: coinbase.rest.RESTClient.preview_order +.. autofunction:: coinbase.rest.RESTClient.preview_market_order +.. autofunction:: coinbase.rest.RESTClient.preview_market_order_buy +.. autofunction:: coinbase.rest.RESTClient.preview_market_order_sell +.. autofunction:: coinbase.rest.RESTClient.preview_limit_order_ioc +.. autofunction:: coinbase.rest.RESTClient.preview_limit_order_ioc_buy +.. autofunction:: coinbase.rest.RESTClient.preview_limit_order_ioc_sell +.. autofunction:: coinbase.rest.RESTClient.preview_limit_order_gtc +.. autofunction:: coinbase.rest.RESTClient.preview_limit_order_gtc_buy +.. autofunction:: coinbase.rest.RESTClient.preview_limit_order_gtc_sell +.. autofunction:: coinbase.rest.RESTClient.preview_limit_order_gtd +.. autofunction:: coinbase.rest.RESTClient.preview_limit_order_gtd_buy +.. autofunction:: coinbase.rest.RESTClient.preview_limit_order_gtd_sell +.. autofunction:: coinbase.rest.RESTClient.preview_limit_order_fok +.. autofunction:: coinbase.rest.RESTClient.preview_limit_order_fok_buy +.. autofunction:: coinbase.rest.RESTClient.preview_limit_order_fok_sell +.. autofunction:: coinbase.rest.RESTClient.preview_stop_limit_order_gtc +.. autofunction:: coinbase.rest.RESTClient.preview_stop_limit_order_gtc_buy +.. autofunction:: coinbase.rest.RESTClient.preview_stop_limit_order_gtc_sell +.. autofunction:: coinbase.rest.RESTClient.preview_stop_limit_order_gtd +.. autofunction:: coinbase.rest.RESTClient.preview_stop_limit_order_gtd_buy +.. autofunction:: coinbase.rest.RESTClient.preview_stop_limit_order_gtd_sell +.. autofunction:: coinbase.rest.RESTClient.preview_trigger_bracket_order_gtc +.. autofunction:: coinbase.rest.RESTClient.preview_trigger_bracket_order_gtc_buy +.. autofunction:: coinbase.rest.RESTClient.preview_trigger_bracket_order_gtc_sell +.. autofunction:: coinbase.rest.RESTClient.preview_trigger_bracket_order_gtd +.. autofunction:: coinbase.rest.RESTClient.preview_trigger_bracket_order_gtd_buy +.. autofunction:: coinbase.rest.RESTClient.preview_trigger_bracket_order_gtd_sell +.. autofunction:: coinbase.rest.RESTClient.close_position Portfolios ------------------------------- -.. automodule:: coinbase.rest.portfolios - :members: - :undoc-members: - :show-inheritance: +.. autofunction:: coinbase.rest.RESTClient.get_portfolios +.. autofunction:: coinbase.rest.RESTClient.create_portfolio +.. autofunction:: coinbase.rest.RESTClient.get_portfolio_breakdown +.. autofunction:: coinbase.rest.RESTClient.move_portfolio_funds +.. autofunction:: coinbase.rest.RESTClient.edit_portfolio +.. autofunction:: coinbase.rest.RESTClient.delete_portfolio Futures ---------------------------- -.. automodule:: coinbase.rest.futures - :members: - :undoc-members: - :show-inheritance: +.. autofunction:: coinbase.rest.RESTClient.get_futures_balance_summary +.. autofunction:: coinbase.rest.RESTClient.list_futures_positions +.. autofunction:: coinbase.rest.RESTClient.get_futures_position +.. autofunction:: coinbase.rest.RESTClient.schedule_futures_sweep +.. autofunction:: coinbase.rest.RESTClient.list_futures_sweeps +.. autofunction:: coinbase.rest.RESTClient.cancel_pending_futures_sweep +.. autofunction:: coinbase.rest.RESTClient.get_intraday_margin_setting +.. autofunction:: coinbase.rest.RESTClient.get_current_margin_window +.. autofunction:: coinbase.rest.RESTClient.set_intraday_margin_setting Perpetuals --------------------------- -.. automodule:: coinbase.rest.perpetuals - :members: - :undoc-members: - :show-inheritance: +.. autofunction:: coinbase.rest.RESTClient.allocate_portfolio +.. autofunction:: coinbase.rest.RESTClient.get_perps_portfolio_summary +.. autofunction:: coinbase.rest.RESTClient.list_perps_positions +.. autofunction:: coinbase.rest.RESTClient.get_perps_position +.. autofunction:: coinbase.rest.RESTClient.get_perps_portfolio_balances +.. autofunction:: coinbase.rest.RESTClient.opt_in_or_out_multi_asset_collateral Fees ------------------------- -.. automodule:: coinbase.rest.fees - :members: - :undoc-members: - :show-inheritance: +.. autofunction:: coinbase.rest.RESTClient.get_transaction_summary Converts ---------------------------- -.. automodule:: coinbase.rest.convert - :members: - :undoc-members: - :show-inheritance: +.. autofunction:: coinbase.rest.RESTClient.create_convert_quote +.. autofunction:: coinbase.rest.RESTClient.get_convert_trade +.. autofunction:: coinbase.rest.RESTClient.commit_convert_trade Public --------------------------- -.. automodule:: coinbase.rest.public - :members: - :undoc-members: - :show-inheritance: +.. autofunction:: coinbase.rest.RESTClient.get_unix_time +.. autofunction:: coinbase.rest.RESTClient.get_public_product_book +.. autofunction:: coinbase.rest.RESTClient.get_public_products +.. autofunction:: coinbase.rest.RESTClient.get_public_product +.. autofunction:: coinbase.rest.RESTClient.get_public_candles +.. autofunction:: coinbase.rest.RESTClient.get_public_market_trades Payments ------------------------------- -.. automodule:: coinbase.rest.payments - :members: - :undoc-members: - :show-inheritance: \ No newline at end of file +.. autofunction:: coinbase.rest.RESTClient.list_payment_methods +.. autofunction:: coinbase.rest.RESTClient.get_payment_method + +Data API +------------------------------- + +.. autofunction:: coinbase.rest.RESTClient.get_api_key_permissions diff --git a/docs/coinbase.websocket.rst b/docs/coinbase.websocket.rst index 6cdab91..87b625c 100644 --- a/docs/coinbase.websocket.rst +++ b/docs/coinbase.websocket.rst @@ -4,52 +4,69 @@ Websocket API Client WSClient Constructor --------------------------- -.. autofunction:: coinbase.websocket.websocket_base.WSBase +.. autoclass:: coinbase.websocket.WSClient WebSocket Utils --------------------------- -.. autofunction:: coinbase.websocket.websocket_base.WSBase.open - -.. autofunction:: coinbase.websocket.websocket_base.WSBase.open_async - -.. autofunction:: coinbase.websocket.websocket_base.WSBase.close - -.. autofunction:: coinbase.websocket.websocket_base.WSBase.close_async - -.. autofunction:: coinbase.websocket.websocket_base.WSBase.subscribe - -.. autofunction:: coinbase.websocket.websocket_base.WSBase.subscribe_async - -.. autofunction:: coinbase.websocket.websocket_base.WSBase.unsubscribe - -.. autofunction:: coinbase.websocket.websocket_base.WSBase.unsubscribe_async - -.. autofunction:: coinbase.websocket.websocket_base.WSBase.unsubscribe_all - -.. autofunction:: coinbase.websocket.websocket_base.WSBase.unsubscribe_all_async - -.. autofunction:: coinbase.websocket.websocket_base.WSBase.sleep_with_exception_check - -.. autofunction:: coinbase.websocket.websocket_base.WSBase.sleep_with_exception_check_async - -.. autofunction:: coinbase.websocket.websocket_base.WSBase.run_forever_with_exception_check - -.. autofunction:: coinbase.websocket.websocket_base.WSBase.run_forever_with_exception_check_async - -.. autofunction:: coinbase.websocket.websocket_base.WSBase.raise_background_exception +.. autofunction:: coinbase.websocket.WSClient.open +.. autofunction:: coinbase.websocket.WSClient.open_async +.. autofunction:: coinbase.websocket.WSClient.close +.. autofunction:: coinbase.websocket.WSClient.close_async +.. autofunction:: coinbase.websocket.WSClient.subscribe +.. autofunction:: coinbase.websocket.WSClient.subscribe_async +.. autofunction:: coinbase.websocket.WSClient.unsubscribe +.. autofunction:: coinbase.websocket.WSClient.unsubscribe_async +.. autofunction:: coinbase.websocket.WSClient.unsubscribe_all +.. autofunction:: coinbase.websocket.WSClient.unsubscribe_all_async +.. autofunction:: coinbase.websocket.WSClient.sleep_with_exception_check +.. autofunction:: coinbase.websocket.WSClient.sleep_with_exception_check_async +.. autofunction:: coinbase.websocket.WSClient.run_forever_with_exception_check +.. autofunction:: coinbase.websocket.WSClient.run_forever_with_exception_check_async +.. autofunction:: coinbase.websocket.WSClient.raise_background_exception Channels ----------------------------- -.. automodule:: coinbase.websocket.channels - :members: - :undoc-members: - :show-inheritance: +.. autofunction:: coinbase.websocket.WSClient.heartbeats +.. autofunction:: coinbase.websocket.WSClient.heartbeats_async +.. autofunction:: coinbase.websocket.WSClient.heartbeats_unsubscribe +.. autofunction:: coinbase.websocket.WSClient.heartbeats_unsubscribe_async +.. autofunction:: coinbase.websocket.WSClient.candles +.. autofunction:: coinbase.websocket.WSClient.candles_async +.. autofunction:: coinbase.websocket.WSClient.candles_unsubscribe +.. autofunction:: coinbase.websocket.WSClient.candles_unsubscribe_async +.. autofunction:: coinbase.websocket.WSClient.market_trades +.. autofunction:: coinbase.websocket.WSClient.market_trades_async +.. autofunction:: coinbase.websocket.WSClient.market_trades_unsubscribe +.. autofunction:: coinbase.websocket.WSClient.market_trades_unsubscribe_async +.. autofunction:: coinbase.websocket.WSClient.status +.. autofunction:: coinbase.websocket.WSClient.status_async +.. autofunction:: coinbase.websocket.WSClient.status_unsubscribe +.. autofunction:: coinbase.websocket.WSClient.status_unsubscribe_async +.. autofunction:: coinbase.websocket.WSClient.ticker +.. autofunction:: coinbase.websocket.WSClient.ticker_async +.. autofunction:: coinbase.websocket.WSClient.ticker_unsubscribe +.. autofunction:: coinbase.websocket.WSClient.ticker_unsubscribe_async +.. autofunction:: coinbase.websocket.WSClient.ticker_batch +.. autofunction:: coinbase.websocket.WSClient.ticker_batch_async +.. autofunction:: coinbase.websocket.WSClient.ticker_batch_unsubscribe +.. autofunction:: coinbase.websocket.WSClient.ticker_batch_unsubscribe_async +.. autofunction:: coinbase.websocket.WSClient.level2 +.. autofunction:: coinbase.websocket.WSClient.level2_async +.. autofunction:: coinbase.websocket.WSClient.level2_unsubscribe +.. autofunction:: coinbase.websocket.WSClient.level2_unsubscribe_async +.. autofunction:: coinbase.websocket.WSClient.user +.. autofunction:: coinbase.websocket.WSClient.user_async +.. autofunction:: coinbase.websocket.WSClient.user_unsubscribe +.. autofunction:: coinbase.websocket.WSClient.user_unsubscribe_async +.. autofunction:: coinbase.websocket.WSClient.futures_balance_summary +.. autofunction:: coinbase.websocket.WSClient.futures_balance_summary_async +.. autofunction:: coinbase.websocket.WSClient.futures_balance_summary_unsubscribe +.. autofunction:: coinbase.websocket.WSClient.futures_balance_summary_unsubscribe_async Exceptions --------------------------- -.. autofunction:: coinbase.websocket.websocket_base.WSClientException - -.. autofunction:: coinbase.websocket.websocket_base.WSClientConnectionClosedException +.. autofunction:: coinbase.websocket.WSClientException +.. autofunction:: coinbase.websocket.WSClientConnectionClosedException diff --git a/docs/index.rst b/docs/index.rst index 478618b..a8b08c0 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -19,14 +19,14 @@ Getting Started :alt: Apache License 2.0 Welcome to the official Coinbase Advanced API Python SDK. This python project was created to allow coders to easily plug -into the `Coinbase Advanced API `_ +into the `Coinbase Advanced API `_ -- Docs: https://docs.cloud.coinbase.com/advanced-trade-api/docs/welcome +- Docs: https://docs.cdp.coinbase.com/advanced-trade/docs/welcome/ - Python SDK: https://github.com/coinbase/coinbase-advanced-py For detailed exercises on how to get started using the SDK look at our SDK Overview: -https://docs.cloud.coinbase.com/advanced-trade-api/docs/sdk-overview +https://docs.cdp.coinbase.com/advanced-trade/docs/sdk-overview ------------- diff --git a/tests/rest/test_data_api.py b/tests/rest/test_data_api.py new file mode 100644 index 0000000..3003335 --- /dev/null +++ b/tests/rest/test_data_api.py @@ -0,0 +1,29 @@ +import unittest + +from requests_mock import Mocker + +from coinbase.rest import RESTClient + +from ..constants import TEST_API_KEY, TEST_API_SECRET + + +class DataApiTest(unittest.TestCase): + def test_get_api_key_permissions(self): + client = RESTClient(TEST_API_KEY, TEST_API_SECRET) + + expected_response = { + "can_view": True, + "can_trade": False, + "can_withdraw": False, + "portfolio_uuid": "portfolio1", + } + + with Mocker() as m: + m.request( + "GET", + "https://api.coinbase.com/api/v3/brokerage/key_permissions", + json=expected_response, + ) + key_permissions = client.get_api_key_permissions() + + self.assertEqual(key_permissions, expected_response) diff --git a/tests/rest/test_futures.py b/tests/rest/test_futures.py index 3644839..970bcce 100644 --- a/tests/rest/test_futures.py +++ b/tests/rest/test_futures.py @@ -8,38 +8,6 @@ class FuturesTest(unittest.TestCase): - def test_close_position(self): - client = RESTClient(TEST_API_KEY, TEST_API_SECRET) - - expected_response = { - "client_order_id": "client_order_id_1", - "product_id": "product_id_1", - } - - with Mocker() as m: - m.request( - "POST", - "https://api.coinbase.com/api/v3/brokerage/orders/close_position", - json=expected_response, - ) - closedOrder = client.close_position( - "client_order_id_1", "product_id_1", "100" - ) - - captured_request = m.request_history[0] - captured_json = captured_request.json() - - self.assertEqual(captured_request.query, "") - self.assertEqual( - captured_json, - { - "client_order_id": "client_order_id_1", - "product_id": "product_id_1", - "size": "100", - }, - ) - self.assertEqual(closedOrder, expected_response) - def test_get_futures_balance_summary(self): client = RESTClient(TEST_API_KEY, TEST_API_SECRET) diff --git a/tests/rest/test_market_data.py b/tests/rest/test_market_data.py index 717fedb..c0d2b99 100644 --- a/tests/rest/test_market_data.py +++ b/tests/rest/test_market_data.py @@ -20,14 +20,14 @@ def test_get_candles(self): json=expected_response, ) candles = client.get_candles( - "product_id_1", "1640995200", "1641081600", "FIVE_MINUTE" + "product_id_1", "1640995200", "1641081600", "FIVE_MINUTE", 2 ) captured_request = m.request_history[0] self.assertEqual( captured_request.query, - "start=1640995200&end=1641081600&granularity=five_minute", + "start=1640995200&end=1641081600&granularity=five_minute&limit=2", ) self.assertEqual(candles, expected_response) diff --git a/tests/rest/test_orders.py b/tests/rest/test_orders.py index 91928bd..92e8877 100644 --- a/tests/rest/test_orders.py +++ b/tests/rest/test_orders.py @@ -1222,9 +1222,6 @@ def test_preview_order(self): "product_id_1", "BUY", order_configuration, - commission_rate="0.005", - is_max=False, - tradable_balance="100", leverage="5", margin_type="CROSS", retail_portfolio_id="portfolio_id_1", @@ -1240,9 +1237,6 @@ def test_preview_order(self): "product_id": "product_id_1", "side": "BUY", "order_configuration": {"market_market_ioc": {"quote_size": "1"}}, - "commission_rate": {"value": "0.005"}, - "is_max": False, - "tradable_balance": "100", "leverage": "5", "margin_type": "CROSS", "retail_portfolio_id": "portfolio_id_1", @@ -1274,7 +1268,6 @@ def test_preview_market_order(self): "product_id": "product_id_1", "side": "BUY", "order_configuration": {"market_market_ioc": {"quote_size": "1"}}, - "is_max": False, }, ) self.assertEqual(preview, expected_response) @@ -1303,7 +1296,6 @@ def test_preview_market_order_buy(self): "product_id": "product_id_1", "side": "BUY", "order_configuration": {"market_market_ioc": {"quote_size": "1"}}, - "is_max": False, }, ) self.assertEqual(preview, expected_response) @@ -1332,7 +1324,6 @@ def test_preview_market_order_sell(self): "product_id": "product_id_1", "side": "SELL", "order_configuration": {"market_market_ioc": {"base_size": "1"}}, - "is_max": False, }, ) self.assertEqual(preview, expected_response) @@ -1362,7 +1353,6 @@ def test_preview_limit_order_ioc(self): "order_configuration": { "sor_limit_ioc": {"base_size": "1", "limit_price": "100"} }, - "is_max": False, }, ) self.assertEqual(preview, expected_response) @@ -1392,7 +1382,6 @@ def test_preview_limit_order_ioc_buy(self): "order_configuration": { "sor_limit_ioc": {"base_size": "1", "limit_price": "100"} }, - "is_max": False, }, ) self.assertEqual(preview, expected_response) @@ -1422,7 +1411,6 @@ def test_preview_limit_order_ioc_sell(self): "order_configuration": { "sor_limit_ioc": {"base_size": "1", "limit_price": "100"} }, - "is_max": False, }, ) self.assertEqual(preview, expected_response) @@ -1462,7 +1450,6 @@ def test_preview_limit_order_gtc(self): "post_only": True, } }, - "is_max": False, }, ) self.assertEqual(preview, expected_response) @@ -1501,7 +1488,6 @@ def test_preview_limit_order_gtc_buy(self): "post_only": True, } }, - "is_max": False, }, ) self.assertEqual(preview, expected_response) @@ -1540,7 +1526,6 @@ def test_preview_limit_order_gtc_sell(self): "post_only": True, } }, - "is_max": False, }, ) self.assertEqual(preview, expected_response) @@ -1581,7 +1566,6 @@ def test_preview_limit_order_gtd(self): "post_only": False, } }, - "is_max": False, }, ) self.assertEqual(preview, expected_response) @@ -1618,7 +1602,6 @@ def test_preview_limit_order_gtd_buy(self): "post_only": False, } }, - "is_max": False, }, ) self.assertEqual(preview, expected_response) @@ -1655,7 +1638,6 @@ def test_preview_limit_order_gtd_sell(self): "post_only": False, } }, - "is_max": False, }, ) self.assertEqual(preview, expected_response) @@ -1685,7 +1667,6 @@ def test_preview_limit_order_fok(self): "order_configuration": { "limit_limit_fok": {"base_size": "1", "limit_price": "100"} }, - "is_max": False, }, ) self.assertEqual(preview, expected_response) @@ -1715,7 +1696,6 @@ def test_preview_limit_order_fok_buy(self): "order_configuration": { "limit_limit_fok": {"base_size": "1", "limit_price": "100"} }, - "is_max": False, }, ) self.assertEqual(preview, expected_response) @@ -1745,7 +1725,6 @@ def test_preview_limit_order_fok_sell(self): "order_configuration": { "limit_limit_fok": {"base_size": "1", "limit_price": "100"} }, - "is_max": False, }, ) self.assertEqual(preview, expected_response) @@ -1787,7 +1766,6 @@ def test_preview_stop_limit_order_gtc(self): "stop_direction": "STOP_DIRECTION_STOP_UP", } }, - "is_max": False, }, ) self.assertEqual(preview, expected_response) @@ -1828,7 +1806,6 @@ def test_preview_stop_limit_order_gtc_buy(self): "stop_direction": "STOP_DIRECTION_STOP_UP", } }, - "is_max": False, }, ) self.assertEqual(preview, expected_response) @@ -1869,7 +1846,6 @@ def test_preview_stop_limit_order_gtc_sell(self): "stop_direction": "STOP_DIRECTION_STOP_UP", } }, - "is_max": False, }, ) self.assertEqual(preview, expected_response) @@ -1913,7 +1889,6 @@ def test_preview_stop_limit_order_gtd(self): "stop_direction": "STOP_DIRECTION_STOP_UP", } }, - "is_max": False, }, ) self.assertEqual(preview, expected_response) @@ -1956,7 +1931,6 @@ def test_preview_stop_limit_order_gtd_buy(self): "stop_direction": "STOP_DIRECTION_STOP_UP", } }, - "is_max": False, }, ) self.assertEqual(preview, expected_response) @@ -1999,7 +1973,6 @@ def test_preview_stop_limit_order_gtd_sell(self): "stop_direction": "STOP_DIRECTION_STOP_UP", } }, - "is_max": False, }, ) self.assertEqual(preview, expected_response) @@ -2039,7 +2012,6 @@ def test_preview_trigger_bracket_order_gtc(self): "stop_trigger_price": "90", } }, - "is_max": False, }, ) self.assertEqual(preview, expected_response) @@ -2075,7 +2047,6 @@ def test_preview_trigger_bracket_order_gtc_buy(self): "stop_trigger_price": "90", } }, - "is_max": False, }, ) self.assertEqual(preview, expected_response) @@ -2111,7 +2082,6 @@ def test_preview_trigger_bracket_gtc_sell(self): "stop_trigger_price": "90", } }, - "is_max": False, }, ) self.assertEqual(preview, expected_response) @@ -2153,7 +2123,6 @@ def test_preview_trigger_bracket_order_gtd(self): "end_time": "2022-01-01T00:00:00Z", } }, - "is_max": False, }, ) self.assertEqual(preview, expected_response) @@ -2194,7 +2163,6 @@ def test_preview_trigger_bracket_order_gtd_buy(self): "end_time": "2022-01-01T00:00:00Z", } }, - "is_max": False, }, ) self.assertEqual(preview, expected_response) @@ -2231,7 +2199,38 @@ def test_preview_trigger_bracket_gtd_sell(self): "end_time": "2022-01-01T00:00:00Z", } }, - "is_max": False, }, ) self.assertEqual(preview, expected_response) + + def test_close_position(self): + client = RESTClient(TEST_API_KEY, TEST_API_SECRET) + + expected_response = { + "client_order_id": "client_order_id_1", + "product_id": "product_id_1", + } + + with Mocker() as m: + m.request( + "POST", + "https://api.coinbase.com/api/v3/brokerage/orders/close_position", + json=expected_response, + ) + closedOrder = client.close_position( + "client_order_id_1", "product_id_1", "100" + ) + + captured_request = m.request_history[0] + captured_json = captured_request.json() + + self.assertEqual(captured_request.query, "") + self.assertEqual( + captured_json, + { + "client_order_id": "client_order_id_1", + "product_id": "product_id_1", + "size": "100", + }, + ) + self.assertEqual(closedOrder, expected_response) diff --git a/tests/rest/test_portfolios.py b/tests/rest/test_portfolios.py index d960735..404dcd0 100644 --- a/tests/rest/test_portfolios.py +++ b/tests/rest/test_portfolios.py @@ -57,11 +57,11 @@ def test_get_portfolio_breakdown(self): "https://api.coinbase.com/api/v3/brokerage/portfolios/1234", json=expected_response, ) - breakdown = client.get_portfolio_breakdown("1234") + breakdown = client.get_portfolio_breakdown("1234", "USD") captured_request = m.request_history[0] - self.assertEqual(captured_request.query, "") + self.assertEqual(captured_request.query, "currency=usd") self.assertEqual(breakdown, expected_response) def test_move_portfolio_funds(self): diff --git a/tests/rest/test_products.py b/tests/rest/test_products.py index 56cdd9c..2368c34 100644 --- a/tests/rest/test_products.py +++ b/tests/rest/test_products.py @@ -25,7 +25,7 @@ def test_get_products(self): self.assertEqual( captured_request.query, - "limit=2&product_type=spot&get_tradability_status=false", + "limit=2&product_type=spot&get_tradability_status=false&get_all_products=false", ) self.assertEqual(products, expected_response) diff --git a/tests/rest/test_public.py b/tests/rest/test_public.py index 8230a97..1c15e47 100644 --- a/tests/rest/test_public.py +++ b/tests/rest/test_public.py @@ -99,7 +99,10 @@ def test_get_public_products(self): captured_request = m.request_history[0] - self.assertEqual(captured_request.query, "limit=2&product_type=spot") + self.assertEqual( + captured_request.query, + "limit=2&product_type=spot&get_all_products=false", + ) self.assertEqual(products, expected_response) def test_get_public_product(self): @@ -132,14 +135,14 @@ def test_get_public_candles(self): json=expected_response, ) candles = client.get_public_candles( - "product_id_1", "1640995200", "1641081600", "FIVE_MINUTE" + "product_id_1", "1640995200", "1641081600", "FIVE_MINUTE", 2 ) captured_request = m.request_history[0] self.assertEqual( captured_request.query, - "start=1640995200&end=1641081600&granularity=five_minute", + "start=1640995200&end=1641081600&granularity=five_minute&limit=2", ) self.assertEqual(candles, expected_response) diff --git a/tests/test_jwt_generator.py b/tests/test_jwt_generator.py index 6565ae9..41a0279 100644 --- a/tests/test_jwt_generator.py +++ b/tests/test_jwt_generator.py @@ -5,7 +5,6 @@ import jwt from coinbase import jwt_generator -from coinbase.constants import REST_SERVICE, WS_SERVICE from .constants import TEST_API_KEY, TEST_API_SECRET diff --git a/tests/websocket/test_channels.py b/tests/websocket/test_channels.py index 36c2f24..a769086 100644 --- a/tests/websocket/test_channels.py +++ b/tests/websocket/test_channels.py @@ -8,6 +8,7 @@ from coinbase.constants import ( CANDLES, + FUTURES_BALANCE_SUMMARY, HEARTBEATS, LEVEL2, MARKET_TRADES, @@ -20,6 +21,8 @@ from ..constants import TEST_API_KEY, TEST_API_SECRET +NO_PRODUCT_CHANNELS = {HEARTBEATS, FUTURES_BALANCE_SUMMARY} + class WSBaseTest(unittest.IsolatedAsyncioTestCase): async def asyncSetUp(self): @@ -53,23 +56,31 @@ def generic_channel_test( self.assertIsNotNone(self.ws.websocket) # subscribe - channel_func(product_ids=["BTC-USD", "ETH-USD"]) + product_ids = [] + if channel_const not in NO_PRODUCT_CHANNELS: + product_ids = ["BTC-USD", "ETH-USD"] + channel_func(product_ids=product_ids) + else: + channel_func() self.mock_websocket.send.assert_awaited_once() # assert subscribe message subscribe = json.loads(self.mock_websocket.send.call_args_list[0][0][0]) self.assertEqual(subscribe["type"], "subscribe") - self.assertEqual(subscribe["product_ids"], ["BTC-USD", "ETH-USD"]) + self.assertEqual(subscribe["product_ids"], product_ids) self.assertEqual(subscribe["channel"], channel_const) # unsubscribe - channel_func_unsub(product_ids=["BTC-USD", "ETH-USD"]) + if channel_const not in NO_PRODUCT_CHANNELS: + channel_func_unsub(product_ids=product_ids) + else: + channel_func_unsub() self.assertEqual(self.mock_websocket.send.await_count, 2) # assert unsubscribe message unsubscribe = json.loads(self.mock_websocket.send.call_args_list[1][0][0]) self.assertEqual(unsubscribe["type"], "unsubscribe") - self.assertEqual(unsubscribe["product_ids"], ["BTC-USD", "ETH-USD"]) + self.assertEqual(unsubscribe["product_ids"], product_ids) self.assertEqual(unsubscribe["channel"], channel_const) # close @@ -88,23 +99,31 @@ async def generic_channel_test_async( self.assertIsNotNone(self.ws.websocket) # subscribe - await channel_func(product_ids=["BTC-USD", "ETH-USD"]) + product_ids = [] + if channel_const not in NO_PRODUCT_CHANNELS: + product_ids = ["BTC-USD", "ETH-USD"] + await channel_func(product_ids=product_ids) + else: + await channel_func() self.mock_websocket.send.assert_awaited_once() # assert subscribe message subscribe = json.loads(self.mock_websocket.send.call_args_list[0][0][0]) self.assertEqual(subscribe["type"], "subscribe") - self.assertEqual(subscribe["product_ids"], ["BTC-USD", "ETH-USD"]) + self.assertEqual(subscribe["product_ids"], product_ids) self.assertEqual(subscribe["channel"], channel_const) # unsubscribe - await channel_func_unsub(product_ids=["BTC-USD", "ETH-USD"]) + if channel_const not in NO_PRODUCT_CHANNELS: + await channel_func_unsub(product_ids=product_ids) + else: + await channel_func_unsub() self.assertEqual(self.mock_websocket.send.await_count, 2) # assert unsubscribe message unsubscribe = json.loads(self.mock_websocket.send.call_args_list[1][0][0]) self.assertEqual(unsubscribe["type"], "unsubscribe") - self.assertEqual(unsubscribe["product_ids"], ["BTC-USD", "ETH-USD"]) + self.assertEqual(unsubscribe["product_ids"], product_ids) self.assertEqual(unsubscribe["channel"], channel_const) # close @@ -202,3 +221,19 @@ def test_user_async(self): self.ws.user_async, self.ws.user_unsubscribe_async, USER ) ) + + def test_futures_balance_summary(self): + self.generic_channel_test( + self.ws.futures_balance_summary, + self.ws.futures_balance_summary_unsubscribe, + FUTURES_BALANCE_SUMMARY, + ) + + def test_futures_balance_summary_async(self): + asyncio.run( + self.generic_channel_test_async( + self.ws.futures_balance_summary_async, + self.ws.futures_balance_summary_unsubscribe_async, + FUTURES_BALANCE_SUMMARY, + ) + )