├── setup.cfg ├── tests ├── __init__.py ├── fixtures │ ├── __init__.py │ ├── delete_portfolio_success_response.json │ ├── get_unix_time_success_response.json │ ├── move_funds_success_response.json │ ├── create_portfolio_success_response.json │ ├── edit_order_success_response.json │ ├── edit_portfolio_success_response.json │ ├── list_portfolios_success_response.json │ ├── edit_order_preview_success_response.json │ ├── cancel_orders_success_response.json │ ├── create_buy_market_order_success_response.json │ ├── create_sell_market_order_success_response.json │ ├── default_failure_response.json │ ├── default_order_failure_response.json │ ├── create_order_failure_no_funds_response.json │ ├── create_limit_order_success_response.json │ ├── get_transactions_summary_success_response.json │ ├── get_account_success_response.json │ ├── create_stop_limit_order_success_response.json │ ├── get_best_bid_ask_success_response.json │ ├── get_product_success_response.json │ ├── get_product_book_success_response.json │ ├── get_order_success_response.json │ ├── list_fills_success_response.json │ ├── list_fills_all_call_2_success_response.json │ ├── list_fills_all_call_1_success_response.json │ ├── list_products_success_response.json │ ├── get_product_candles_success_response.json │ ├── fixtures.py │ ├── list_orders_all_call_2_success_response.json │ ├── list_orders_all_call_1_success_response.json │ ├── list_orders_success_response.json │ └── list_orders_with_extra_unnamed_success_response.json └── playground.py ├── coinbaseadvanced ├── models │ ├── __init__.py │ ├── error.py │ ├── futures.py │ ├── common.py │ ├── accounts.py │ ├── fees.py │ ├── portfolios.py │ ├── products.py │ ├── market_data.py │ └── orders.py ├── __init__.py ├── utils.py └── client_websocket.py ├── MANIFEST.in ├── requirements.txt ├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── LICENSE ├── setup.py ├── .gitignore └── README.md /setup.cfg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /coinbaseadvanced/models/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | prune tests* -------------------------------------------------------------------------------- /tests/fixtures/delete_portfolio_success_response.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /coinbaseadvanced/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Package Version 3 | """ 4 | __version__ = '1.2.0' 5 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | cfgv==3.3.1 2 | cryptography==42.0.4 3 | PyJWT==2.8.0 4 | requests==2.32.0 5 | rfc3986==2.0.0 6 | websocket-client==1.8.0 -------------------------------------------------------------------------------- /tests/fixtures/get_unix_time_success_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "iso": "2023-11-28T00:00:22Z", 3 | "epochSeconds": "1701129622", 4 | "epochMillis": "1701129622115" 5 | } -------------------------------------------------------------------------------- /tests/fixtures/move_funds_success_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "source_portfolio_uuid": "klsjdlksd-nsjkdnfk-234234", 3 | "target_portfolio_uuid": "strklsdmkfls-34dfg-ing" 4 | } -------------------------------------------------------------------------------- /tests/fixtures/create_portfolio_success_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "portfolio": { 3 | "name": "portf-test3", 4 | "uuid": "354808f3-06df-42d7-87ec-488f34ff6f14", 5 | "type": "CONSUMER", 6 | "deleted": false 7 | } 8 | } -------------------------------------------------------------------------------- /tests/fixtures/edit_order_success_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "success": true, 3 | "errors": { 4 | "edit_failure_reason": "UNKNOWN_EDIT_ORDER_FAILURE_REASON", 5 | "preview_failure_reason": "UNKNOWN_PREVIEW_FAILURE_REASON" 6 | } 7 | } -------------------------------------------------------------------------------- /tests/fixtures/edit_portfolio_success_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "portfolio": { 3 | "name": "edited-portfolio-name", 4 | "uuid": "354808f3-06df-42d7-87ec-488f34ff6f14", 5 | "type": "CONSUMER", 6 | "deleted": false 7 | } 8 | } -------------------------------------------------------------------------------- /tests/fixtures/list_portfolios_success_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "portfolios": [ 3 | { 4 | "name": "Default", 5 | "uuid": "cd5d3286-b5ad-5f48-b356-1fd3e4e73f72", 6 | "type": "DEFAULT", 7 | "deleted": false 8 | } 9 | ] 10 | } -------------------------------------------------------------------------------- /tests/fixtures/edit_order_preview_success_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "errors": [], 3 | "slippage": "string", 4 | "order_total": "string", 5 | "commission_total": "string", 6 | "quote_size": "string", 7 | "base_size": "string", 8 | "best_bid": "string", 9 | "best_ask": "string", 10 | "average_filled_price": "string" 11 | } -------------------------------------------------------------------------------- /tests/fixtures/cancel_orders_success_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "results": [ 3 | { 4 | "success": false, 5 | "failure_reason": "UNKNOWN_CANCEL_ORDER", 6 | "order_id": "dc98ccb7-8494-418d-99e9-651b76ca64f2" 7 | }, 8 | { 9 | "success": true, 10 | "failure_reason": "UNKNOWN_CANCEL_FAILURE_REASON", 11 | "order_id": "2ab5a57e-f3e4-41fa-bf82-65e7d89640c0" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /tests/fixtures/create_buy_market_order_success_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "success": true, 3 | "failure_reason": "UNKNOWN_FAILURE_REASON", 4 | "order_id": "1f71a67f-6964-4a58-9438-411a5a6f22fc", 5 | "success_response": { 6 | "order_id": "1f71a67f-6964-4a58-9438-411a5a6f22fc", 7 | "product_id": "ALGO-USD", 8 | "side": "BUY", 9 | "client_order_id": "asdasd" 10 | }, 11 | "order_configuration": { 12 | "market_market_ioc": { 13 | "quote_size": "1" 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /tests/fixtures/create_sell_market_order_success_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "success": true, 3 | "failure_reason": "UNKNOWN_FAILURE_REASON", 4 | "order_id": "95a50b31-7128-49ac-bba9-0e7200051a92", 5 | "success_response": { 6 | "order_id": "95a50b31-7128-49ac-bba9-0e7200051a92", 7 | "product_id": "ALGO-USD", 8 | "side": "SELL", 9 | "client_order_id": "njkasdh7" 10 | }, 11 | "order_configuration": { 12 | "market_market_ioc": { 13 | "base_size": "5" 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /tests/fixtures/default_failure_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "success": false, 3 | "failure_reason": "UNKNOWN_FAILURE_REASON", 4 | "order_id": "", 5 | "error_response": { 6 | "error": "INSUFFICIENT_FUND", 7 | "message": "Insufficient balance in source account", 8 | "error_details": "", 9 | "preview_failure_reason": "PREVIEW_INSUFFICIENT_FUND" 10 | }, 11 | "order_configuration": { 12 | "limit_limit_gtc": { 13 | "base_size": "10000", 14 | "limit_price": ".19", 15 | "post_only": false 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /tests/fixtures/default_order_failure_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "success": false, 3 | "failure_reason": "UNKNOWN_FAILURE_REASON", 4 | "order_id": "", 5 | "error_response": { 6 | "error": "INSUFFICIENT_FUND", 7 | "message": "Insufficient balance in source account", 8 | "error_details": "", 9 | "preview_failure_reason": "PREVIEW_INSUFFICIENT_FUND" 10 | }, 11 | "order_configuration": { 12 | "limit_limit_gtc": { 13 | "base_size": "10000", 14 | "limit_price": ".19", 15 | "post_only": false 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /tests/fixtures/create_order_failure_no_funds_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "success": false, 3 | "failure_reason": "UNKNOWN_FAILURE_REASON", 4 | "order_id": "", 5 | "error_response": { 6 | "error": "INSUFFICIENT_FUND", 7 | "message": "Insufficient balance in source account", 8 | "error_details": "", 9 | "preview_failure_reason": "PREVIEW_INSUFFICIENT_FUND" 10 | }, 11 | "order_configuration": { 12 | "limit_limit_gtc": { 13 | "base_size": "1000", 14 | "limit_price": ".15", 15 | "post_only": false 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /tests/fixtures/create_limit_order_success_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "success": true, 3 | "failure_reason": "UNKNOWN_FAILURE_REASON", 4 | "order_id": "07f1e718-8ea8-4ece-a2e1-3f00aad7f040", 5 | "success_response": { 6 | "order_id": "07f1e718-8ea8-4ece-a2e1-3f00aad7f040", 7 | "product_id": "ALGO-USD", 8 | "side": "BUY", 9 | "client_order_id": "lknalksdj89asdkl" 10 | }, 11 | "order_configuration": { 12 | "limit_limit_gtc": { 13 | "base_size": "5", 14 | "limit_price": ".19", 15 | "post_only": false 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /tests/fixtures/get_transactions_summary_success_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "total_volume": 16.711822509765625, 3 | "total_fees": 0.09837093204259872, 4 | "fee_tier": { 5 | "pricing_tier": "", 6 | "usd_from": "0", 7 | "usd_to": "10000", 8 | "taker_fee_rate": "0.006", 9 | "maker_fee_rate": "0.004" 10 | }, 11 | "margin_rate": null, 12 | "goods_and_services_tax": null, 13 | "advanced_trade_only_volume": 16.711822509765625, 14 | "advanced_trade_only_fees": 0.09837093204259872, 15 | "coinbase_pro_volume": 0, 16 | "coinbase_pro_fees": 0, 17 | "total_balance": "", 18 | "has_promo_fee": false 19 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /tests/fixtures/get_account_success_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "account": { 3 | "uuid": "b044449a-38a3-5b8f-a506-4a65c9853222", 4 | "name": "BTC Wallet", 5 | "currency": "BTC", 6 | "available_balance": { 7 | "value": "0.2430140900000000", 8 | "currency": "BTC" 9 | }, 10 | "default": true, 11 | "active": true, 12 | "created_at": "2021-02-12T06:25:40.515Z", 13 | "updated_at": "2022-12-26T19:27:01.554Z", 14 | "deleted_at": null, 15 | "type": "ACCOUNT_TYPE_CRYPTO", 16 | "ready": false, 17 | "hold": { 18 | "value": "0.0000000000000000", 19 | "currency": "BTC" 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /tests/fixtures/create_stop_limit_order_success_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "success": true, 3 | "failure_reason": "UNKNOWN_FAILURE_REASON", 4 | "order_id": "1a88f3f2-1a02-4812-a227-a3d2c00e45ce", 5 | "success_response": { 6 | "order_id": "1a88f3f2-1a02-4812-a227-a3d2c00e45ce", 7 | "product_id": "ALGO-USD", 8 | "side": "BUY", 9 | "client_order_id": "mklansdu8wehr" 10 | }, 11 | "order_configuration": { 12 | "stop_limit_stop_limit_gtd": { 13 | "base_size": "7", 14 | "limit_price": "0.16", 15 | "stop_price": "0.18", 16 | "end_time": "2023-05-09T15:00:00Z", 17 | "stop_direction": "STOP_DIRECTION_STOP_DOWN" 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /coinbaseadvanced/models/error.py: -------------------------------------------------------------------------------- 1 | """ 2 | Encapsulating error types. 3 | """ 4 | 5 | import json 6 | import requests 7 | 8 | 9 | class CoinbaseAdvancedTradeAPIError(Exception): 10 | """ 11 | Class CoinbaseAdvancedTradeAPIError is derived from super class Exception 12 | and represent the default generic error when endpoint request fail. 13 | """ 14 | 15 | def __init__(self, error_dict: dict): 16 | self.error_dict = error_dict 17 | 18 | def __str__(self): 19 | return str(self.error_dict) 20 | 21 | @classmethod 22 | def not_ok_response(cls, response: requests.Response) -> 'CoinbaseAdvancedTradeAPIError': 23 | """ 24 | Factory Method for Coinbase Advanced errors. 25 | """ 26 | 27 | try: 28 | error_result = json.loads(response.text) 29 | except ValueError: 30 | error_result = {'reason': response.text} 31 | 32 | return cls(error_dict=error_result) 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /tests/fixtures/get_best_bid_ask_success_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "pricebooks": [ 3 | { 4 | "product_id": "BTC-USD", 5 | "bids": [ 6 | { 7 | "price": "41931.59", 8 | "size": "0.11924182" 9 | } 10 | ], 11 | "asks": [ 12 | { 13 | "price": "41933.63", 14 | "size": "0.195" 15 | } 16 | ], 17 | "time": "2023-12-30T03:44:12.783299Z" 18 | }, 19 | { 20 | "product_id": "ETH-USD", 21 | "bids": [ 22 | { 23 | "price": "2295.05", 24 | "size": "0.26142822" 25 | } 26 | ], 27 | "asks": [ 28 | { 29 | "price": "2295.13", 30 | "size": "0.1089284" 31 | } 32 | ], 33 | "time": "2023-12-30T03:44:12.939329Z" 34 | } 35 | ] 36 | } -------------------------------------------------------------------------------- /tests/fixtures/get_product_success_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "product_id": "BTC-USD", 3 | "price": "23005.84", 4 | "price_percentage_change_24h": "-0.51950230865892", 5 | "volume_24h": "17976.01104373", 6 | "volume_percentage_change_24h": "-31.5074979901098", 7 | "base_increment": "0.00000001", 8 | "quote_increment": "0.01", 9 | "quote_min_size": "1", 10 | "quote_max_size": "50000000", 11 | "base_min_size": "0.000016", 12 | "base_max_size": "2600", 13 | "base_name": "Bitcoin", 14 | "quote_name": "US Dollar", 15 | "watched": false, 16 | "is_disabled": false, 17 | "new": false, 18 | "status": "online", 19 | "cancel_only": false, 20 | "limit_only": false, 21 | "post_only": false, 22 | "trading_disabled": false, 23 | "auction_mode": false, 24 | "product_type": "SPOT", 25 | "quote_currency_id": "USD", 26 | "base_currency_id": "BTC", 27 | "fcm_trading_session_details": null, 28 | "mid_market_price": "", 29 | "alias": "", 30 | "alias_to": [], 31 | "base_display_symbol": "BTC", 32 | "quote_display_symbol": "USD" 33 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Camilo Quintas Meneses 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/fixtures/get_product_book_success_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "pricebook": { 3 | "product_id": "BTC-USD", 4 | "bids": [ 5 | { 6 | "price": "43913.56", 7 | "size": "0.04191551" 8 | }, 9 | { 10 | "price": "43911", 11 | "size": "0.2" 12 | }, 13 | { 14 | "price": "43910.67", 15 | "size": "0.06945405" 16 | }, 17 | { 18 | "price": "43910.66", 19 | "size": "0.29841949" 20 | }, 21 | { 22 | "price": "43910.63", 23 | "size": "0.1138502" 24 | } 25 | ], 26 | "asks": [ 27 | { 28 | "price": "43913.57", 29 | "size": "0.50554" 30 | }, 31 | { 32 | "price": "43916.84", 33 | "size": "0.00218754" 34 | }, 35 | { 36 | "price": "43916.86", 37 | "size": "0.06945446" 38 | }, 39 | { 40 | "price": "43916.87", 41 | "size": "0.17" 42 | }, 43 | { 44 | "price": "43917.1", 45 | "size": "0.24125221" 46 | } 47 | ], 48 | "time": "2024-01-06T19:35:04.617630Z" 49 | } 50 | } -------------------------------------------------------------------------------- /tests/fixtures/get_order_success_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "order": { 3 | "order_id": "nkj23kj234234", 4 | "product_id": "ALGO-USD", 5 | "user_id": "mknl23nlk234lk234", 6 | "order_configuration": { 7 | "limit_limit_gtc": { 8 | "base_size": "5", 9 | "limit_price": "0.15", 10 | "post_only": false 11 | } 12 | }, 13 | "side": "BUY", 14 | "client_order_id": "WoKEnVudA77LvxR4Uknt", 15 | "status": "CANCELLED", 16 | "time_in_force": "GOOD_UNTIL_CANCELLED", 17 | "created_time": "2023-12-29T02:42:39.309158Z", 18 | "completion_percentage": "0", 19 | "filled_size": "0", 20 | "average_filled_price": "0", 21 | "fee": "", 22 | "number_of_fills": "0", 23 | "filled_value": "0", 24 | "pending_cancel": false, 25 | "size_in_quote": false, 26 | "total_fees": "0", 27 | "size_inclusive_of_fees": false, 28 | "total_value_after_fees": "0", 29 | "trigger_status": "INVALID_ORDER_TYPE", 30 | "order_type": "LIMIT", 31 | "reject_reason": "REJECT_REASON_UNSPECIFIED", 32 | "settled": false, 33 | "product_type": "SPOT", 34 | "reject_message": "", 35 | "cancel_message": "User requested cancel", 36 | "order_placement_source": "RETAIL_ADVANCED", 37 | "outstanding_hold_amount": "0", 38 | "is_liquidation": false, 39 | "last_fill_time": null, 40 | "edit_history": [], 41 | "leverage": "", 42 | "margin_type": "UNKNOWN_MARGIN_TYPE" 43 | } 44 | } -------------------------------------------------------------------------------- /coinbaseadvanced/utils.py: -------------------------------------------------------------------------------- 1 | import time 2 | import hashlib 3 | import os 4 | import jwt 5 | from cryptography.hazmat.primitives import serialization 6 | 7 | 8 | def generate_jwt(api_key: str, signing_key: str) -> str: 9 | """ 10 | Generates a JSON Web Token (JWT) for authenticating with the Coinbase API. 11 | 12 | :param api_key: The API key for Coinbase. 13 | :param signing_key: The signing key in PEM format used to sign the JWT. 14 | :return: A JWT token as a string. 15 | :raises ValueError: If there is an issue with loading the private key or encoding the JWT. 16 | """ 17 | # Load the private key from the signing key string 18 | try: 19 | private_key_bytes = signing_key.encode('utf-8') 20 | private_key = serialization.load_pem_private_key(private_key_bytes, password=None) 21 | except Exception as e: 22 | raise ValueError(f"Failed to load private key: {e}") from e 23 | 24 | # Create the JWT payload with issuer, not before, expiry, and subject claims 25 | payload = { 26 | "iss": "coinbase-cloud", 27 | "nbf": int(time.time()), 28 | "exp": int(time.time()) + 120, 29 | "sub": api_key, 30 | } 31 | 32 | # Create JWT headers with key ID and nonce 33 | headers = { 34 | "kid": api_key, 35 | "nonce": hashlib.sha256(os.urandom(16)).hexdigest() 36 | } 37 | 38 | # Encode the JWT using the ES256 algorithm 39 | try: 40 | token = jwt.encode(payload, private_key, algorithm="ES256", headers=headers) 41 | except Exception as e: 42 | raise ValueError(f"Failed to encode JWT: {e}") 43 | 44 | return token 45 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Package Setup Configurations. 3 | """ 4 | 5 | import os 6 | from setuptools import find_packages, setup 7 | 8 | import coinbaseadvanced 9 | 10 | root = os.path.abspath(os.path.dirname(__file__)) 11 | 12 | with open(os.path.join(root, "requirements.txt"), "r", encoding="utf-8") as fh: 13 | requirements = fh.readlines() 14 | 15 | with open("README.md", "r", encoding="utf-8") as fh: 16 | readme = fh.read() 17 | 18 | setup( 19 | name='coinbaseadvanced', 20 | version=coinbaseadvanced.__version__, 21 | packages=find_packages(exclude=("tests",)), 22 | include_package_data=True, 23 | license='MIT', 24 | description='Coinbase Advanced Trade API client library.', 25 | long_description=readme, 26 | long_description_content_type="text/markdown", 27 | url='https://github.com/KmiQ/coinbase-advanced-python/', 28 | download_url=f"https://github.com/KmiQ/coinbase-advanced-python/archive/refs/tags/{coinbaseadvanced.__version__}.tar.gz", 29 | author='Camilo Quintas', 30 | author_email='kmiloc89@gmail.com', 31 | keywords=['api', 'coinbase', 'bitcoin', 'client', 'crypto'], 32 | install_requires=[req for req in requirements], 33 | classifiers=[ 34 | "Development Status :: 5 - Production/Stable", 35 | "Intended Audience :: Developers", 36 | "Intended Audience :: Financial and Insurance Industry", 37 | "Natural Language :: English", 38 | "License :: OSI Approved :: MIT License", 39 | "Programming Language :: Python :: 3.8", 40 | "Operating System :: OS Independent", 41 | "Topic :: Software Development :: Libraries :: Python Modules" 42 | ], 43 | ) 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | Scripts/ 22 | var/ 23 | wheels/ 24 | pip-wheel-metadata/ 25 | share/python-wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | pyvenv.cfg 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | *.py,cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | db.sqlite3-journal 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | .python-version 88 | 89 | # pipenv 90 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 91 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 92 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 93 | # install all needed dependencies. 94 | #Pipfile.lock 95 | 96 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 97 | __pypackages__/ 98 | 99 | # Celery stuff 100 | celerybeat-schedule 101 | celerybeat.pid 102 | 103 | # SageMath parsed files 104 | *.sage.py 105 | 106 | # Environments 107 | .env 108 | .venv 109 | env/ 110 | venv/ 111 | ENV/ 112 | env.bak/ 113 | venv.bak/ 114 | 115 | # Spyder project settings 116 | .spyderproject 117 | .spyproject 118 | 119 | # Rope project settings 120 | .ropeproject 121 | 122 | # mkdocs documentation 123 | /site 124 | 125 | # mypy 126 | .mypy_cache/ 127 | .dmypy.json 128 | dmypy.json 129 | 130 | # Pyre type checker 131 | .pyre/ 132 | 133 | ### VisualStudioCode ### 134 | .vscode 135 | .vscode/ 136 | -------------------------------------------------------------------------------- /coinbaseadvanced/models/futures.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains the definition of the FuturesPosition class and related enums. 3 | """ 4 | 5 | from enum import Enum 6 | from coinbaseadvanced.models.common import BaseModel 7 | 8 | 9 | class MarginType(Enum): 10 | """ 11 | Enum representing the margin type for futures trading. 12 | """ 13 | 14 | UNSPECIFIED = "MARGIN_TYPE_UNSPECIFIED" 15 | CROSS = "MARGIN_TYPE_CROSS" 16 | ISOLATED = "MARGIN_TYPE_ISOLATED" 17 | 18 | 19 | class FuturesPositionSide(Enum): 20 | """ 21 | Enum representing the position side for futures contracts. 22 | """ 23 | 24 | UNSPECIFIED = "FUTURES_POSITION_SIDE_UNSPECIFIED" 25 | LONG = "FUTURES_POSITION_SIDE_LONG" 26 | SHORT = "FUTURES_POSITION_SIDE_SHORT" 27 | 28 | 29 | class FuturesPosition(BaseModel): 30 | """ 31 | Represents a futures position. 32 | 33 | Attributes: 34 | product_id (str): The ID of the product. 35 | contract_size (str): The size of the contract. 36 | side (FuturesPositionSide): The side of the position. 37 | amount (str): The amount of the position. 38 | avg_entry_price (str): The average entry price of the position. 39 | current_price (str): The current price of the position. 40 | unrealized_pnl (str): The unrealized profit/loss of the position. 41 | expiry (str): The expiry date of the position. 42 | underlying_asset (str): The underlying asset of the position. 43 | asset_img_url (str): The URL of the asset's image. 44 | product_name (str): The name of the product. 45 | venue (str): The venue of the position. 46 | notional_value (str): The notional value of the position. 47 | """ 48 | 49 | def __init__( 50 | self, product_id: str, 51 | contract_size: str, 52 | side: str, 53 | amount: str, 54 | avg_entry_price: str, 55 | current_price: str, 56 | unrealized_pnl: str, 57 | expiry: str, 58 | underlying_asset: str, 59 | asset_img_url: str, 60 | product_name: str, 61 | venue: str, 62 | notional_value: str, **kwargs) -> None: 63 | self.product_id = product_id 64 | self.contract_size = contract_size 65 | self.side = FuturesPositionSide[side] 66 | self.amount = amount 67 | self.avg_entry_price = avg_entry_price 68 | self.current_price = current_price 69 | self.unrealized_pnl = unrealized_pnl 70 | self.expiry = expiry 71 | self.underlying_asset = underlying_asset 72 | self.asset_img_url = asset_img_url 73 | self.product_name = product_name 74 | self.venue = venue 75 | self.notional_value = notional_value 76 | 77 | self.kwargs = kwargs 78 | -------------------------------------------------------------------------------- /coinbaseadvanced/models/common.py: -------------------------------------------------------------------------------- 1 | """ 2 | Object models for order related endpoints args and response. 3 | """ 4 | 5 | import requests 6 | 7 | from coinbaseadvanced.models.error import CoinbaseAdvancedTradeAPIError 8 | 9 | 10 | class BaseModel: 11 | """ 12 | Base class for models. 13 | """ 14 | 15 | def __str__(self): 16 | attributes = ", ".join( 17 | f"{key}={value}" for key, value in self.__dict__.items()) 18 | return f"{self.__class__.__name__}({attributes})" 19 | 20 | def __repr__(self): 21 | attributes = ", ".join( 22 | f"{key}={value!r}" for key, value in self.__dict__.items()) 23 | return f"{self.__class__.__name__}({attributes})" 24 | 25 | 26 | class EmptyResponse(BaseModel): 27 | """ 28 | Represents an empty response from the Coinbase Advanced Trade API. 29 | 30 | Attributes: 31 | success (bool): Indicates whether the response was successful or not. 32 | """ 33 | 34 | success: bool 35 | 36 | def __init__(self, **kwargs) -> None: 37 | self.success = True 38 | self.kwargs = kwargs 39 | 40 | @classmethod 41 | def from_response(cls, response: requests.Response) -> 'EmptyResponse': 42 | """ 43 | Factory Method that creates an EmptyResponse object from a requests.Response object. 44 | 45 | Args: 46 | response (requests.Response): The response object returned by the API. 47 | 48 | Returns: 49 | EmptyResponse: An instance of the EmptyResponse class. 50 | 51 | Raises: 52 | CoinbaseAdvancedTradeAPIError: If the response is not OK. 53 | """ 54 | 55 | if not response.ok: 56 | raise CoinbaseAdvancedTradeAPIError.not_ok_response(response) 57 | 58 | result = response.json() 59 | return cls(**result) 60 | 61 | 62 | class UnixTime(BaseModel): 63 | """ 64 | Unix time in different formats. 65 | """ 66 | 67 | iso: str 68 | epochSeconds: str 69 | epochMillis: str 70 | 71 | def __init__(self, 72 | iso: str, 73 | epochSeconds: str, 74 | epochMillis: str, 75 | **kwargs 76 | ) -> None: 77 | 78 | self.iso = iso 79 | self.epochSeconds = epochSeconds 80 | self.epochMillis = epochMillis 81 | 82 | self.kwargs = kwargs 83 | 84 | @classmethod 85 | def from_response(cls, response: requests.Response) -> 'UnixTime': 86 | """ 87 | Factory Method. 88 | """ 89 | 90 | if not response.ok: 91 | raise CoinbaseAdvancedTradeAPIError.not_ok_response(response) 92 | 93 | result = response.json() 94 | return cls(**result) 95 | 96 | 97 | class ValueCurrency(BaseModel): 98 | """ 99 | Available Balance object. 100 | """ 101 | 102 | value: str 103 | currency: str 104 | 105 | def __init__(self, value: str, currency: str, **kwargs) -> None: 106 | self.value = value 107 | self.currency = currency 108 | 109 | self.kwargs = kwargs 110 | -------------------------------------------------------------------------------- /coinbaseadvanced/models/accounts.py: -------------------------------------------------------------------------------- 1 | """ 2 | Object models for account related endpoints args and response. 3 | """ 4 | 5 | from uuid import UUID 6 | from datetime import datetime 7 | from typing import List, Optional 8 | import requests 9 | 10 | from coinbaseadvanced.models.common import BaseModel, ValueCurrency 11 | from coinbaseadvanced.models.error import CoinbaseAdvancedTradeAPIError 12 | 13 | 14 | class Account(BaseModel): 15 | """ 16 | Object representing an account. 17 | """ 18 | 19 | uuid: UUID 20 | name: str 21 | currency: str 22 | available_balance: Optional[ValueCurrency] 23 | default: bool 24 | active: bool 25 | created_at: datetime 26 | updated_at: datetime 27 | deleted_at: datetime 28 | type: str 29 | ready: bool 30 | hold: Optional[ValueCurrency] 31 | 32 | def __init__( 33 | self, uuid: UUID, name: str, currency: str, available_balance: dict, default: bool, 34 | active: bool, created_at: datetime, updated_at: datetime, deleted_at: datetime, 35 | type: str, ready: bool, hold: dict, **kwargs) -> None: 36 | self.uuid = uuid 37 | self.name = name 38 | self.currency = currency 39 | self.available_balance = ValueCurrency(**available_balance) \ 40 | if available_balance is not None else None 41 | self.default = default 42 | self.active = active 43 | self.created_at = created_at 44 | self.updated_at = updated_at 45 | self.deleted_at = deleted_at 46 | self.type = type 47 | self.ready = ready 48 | self.hold = ValueCurrency(**hold) if hold is not None else None 49 | 50 | self.kwargs = kwargs 51 | 52 | @classmethod 53 | def from_response(cls, response: requests.Response) -> 'Account': 54 | """ 55 | Factory method. 56 | """ 57 | 58 | if not response.ok: 59 | raise CoinbaseAdvancedTradeAPIError.not_ok_response(response) 60 | 61 | result = response.json() 62 | account_dict = result['account'] 63 | return cls(**account_dict) 64 | 65 | 66 | class AccountsPage(BaseModel): 67 | """ 68 | Page of accounts. 69 | """ 70 | 71 | accounts: List[Account] 72 | has_next: bool 73 | cursor: Optional[str] 74 | size: int 75 | 76 | def __init__(self, 77 | accounts: List[dict], 78 | has_next: bool, 79 | cursor: Optional[str], 80 | size: int, 81 | **kwargs 82 | ) -> None: 83 | 84 | self.accounts = list(map(lambda x: Account(**x), accounts))\ 85 | if accounts is not None else [] 86 | 87 | self.has_next = has_next 88 | self.cursor = cursor 89 | self.size = size 90 | 91 | self.kwargs = kwargs 92 | 93 | @classmethod 94 | def from_response(cls, response: requests.Response) -> 'AccountsPage': 95 | """ 96 | Factory Method. 97 | """ 98 | 99 | if not response.ok: 100 | raise CoinbaseAdvancedTradeAPIError.not_ok_response(response) 101 | 102 | result = response.json() 103 | return cls(**result) 104 | 105 | def __iter__(self): 106 | return self.accounts.__iter__() if self.accounts is not None else [].__iter__() 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Coinbase Advanced 2 | Python library for the Coinbase Advanced Trade API. 3 | 4 | ## Features 5 | - Support for all the [REST API endpoints](https://docs.cloud.coinbase.com/advanced-trade-api/docs/rest-api-overview) through convenient methods. 6 | - Automatic parsing of API responses into relevant Python objects. 7 | - Unit Tests based on real responses using fixtures. 8 | - [Support for Cloud and Legacy Auth Schemas](https://docs.cloud.coinbase.com/advanced-trade-api/docs/rest-api-auth): 9 | - Support for [Cloud API Trading Keys](https://cloud.coinbase.com/access/api) (Recommended) 10 | - Support for [Legacy API Keys](https://www.coinbase.com/settings/api) (Deprecated but supported in this library for backward compatibility reasons) 11 | 12 | ## Example 13 | ``` 14 | from coinbaseadvanced.client import CoinbaseAdvancedTradeAPIClient 15 | 16 | # Creating the client using Clould API Keys. 17 | client = CoinbaseAdvancedTradeAPIClient.from_cloud_api_keys(API_KEY_NAME, PRIVATE_KEY) 18 | 19 | # Listing accounts. 20 | accounts_page = client.list_accounts() 21 | print(accounts_page.size) 22 | 23 | # Creating a limit order. 24 | order_created = client.create_limit_order(client_order_id="lknalksdj89asdkl", product_id="ALGO-USD", side=Side.BUY, limit_price=".19", base_size=5) 25 | ``` 26 | 27 | ## Websocket usage 28 | 29 | Here is a basic example of how to use the CoinbaseWebSocketClient: 30 | 31 | ``` 32 | import asyncio 33 | import time 34 | from client_websocket import CoinbaseWebSocketClient 35 | 36 | def handle_candle_event(event): 37 | print(f"Received event candle: {event}") 38 | 39 | async def main(): 40 | api_key = "your-api-key" 41 | private_key = "-----BEGIN EC PRIVATE KEY-----\n\n-----END EC PRIVATE KEY-----" 42 | 43 | client = CoinbaseWebSocketClient(api_key, private_key) 44 | client.subscribe(["BTC-EUR"], "candles", callback=handle_candle_event) 45 | 46 | while True: 47 | time.sleep(1) 48 | 49 | if __name__ == "__main__": 50 | asyncio.run(main()) 51 | 52 | ``` 53 | 54 | ### Callback Functions 55 | You can define your own callback functions to handle different types of events. The callback function will receive an event object that you can process as needed. 56 | 57 | ### Heartbeat Subscription 58 | For each subscription to a market data channel, a separate heartbeat subscription is automatically created. This helps to ensure that the connection remains open and active. 59 | 60 | ### Concurrencyadding 61 | Each subscription runs in a separate thread to ensure that multiple subscriptions can operate concurrently without blocking each other. 62 | 63 | ### Coinbase API Rate Limits 64 | Before using this library, it is highly recommended to read the Coinbase API rate limits (https://docs.cdp.coinbase.com/advanced-trade/docs/ws-best-practices/) to understand the constraints and avoid exceeding the limits. 65 | 66 | ### Best Practices 67 | It is also recommended to follow the WebSocket best practices (https://docs.cdp.coinbase.com/advanced-trade/docs/ws-best-practices/) provided by Coinbase for optimal performance and reliability. 68 | 69 | ### Subscription Recommendations 70 | If possible, subscribe to one symbol per subscription to help balance the load on the Coinbase server and improve the reliability of your data stream. 71 | 72 | ## Installation 73 | ``` 74 | pip install coinbaseadvanced 75 | ``` 76 | ## Contributing/Development 77 | Any and all contributions are welcome! The process is simple: 78 | 1. Fork repo. 79 | 2. Install Requirements: `pip install -r requirements.txt`. 80 | 3. Make your changes. 81 | 4. Run the test suite `python -m unittest -v`. 82 | 5. Submit a pull request. 83 | -------------------------------------------------------------------------------- /tests/fixtures/list_fills_success_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "fills": [ 3 | { 4 | "entry_id": "da7042594c7570bec6094dbc13384ad6e9d9f5cb12c00ddb174f782b01c3fd65", 5 | "trade_id": "37d31b1e-0238-471f-b1e7-1667a8a849e2", 6 | "order_id": "3f9ad0fa-266e-495e-82d0-04616beb402f", 7 | "trade_time": "2023-01-29T05:25:44.826Z", 8 | "trade_type": "FILL", 9 | "price": "0.2569", 10 | "size": "5", 11 | "commission": "0.007707", 12 | "product_id": "ALGO-USD", 13 | "sequence_timestamp": "2023-01-29T05:25:44.829980Z", 14 | "liquidity_indicator": "TAKER", 15 | "size_in_quote": false, 16 | "user_id": "cd5d3286-b5ad-5f48-b356-1fd3e4e73f72", 17 | "side": "SELL" 18 | }, 19 | { 20 | "entry_id": "9cf80aefa90a33cc205465bb2764e7fa1bc3160f35137b16709f084f04755237", 21 | "trade_id": "b3311a1f-9ec2-4e9d-9ca3-96be13b11ae3", 22 | "order_id": "4866b58d-8baf-4996-b5e5-629f0270dd0a", 23 | "trade_time": "2023-01-29T05:17:29.020Z", 24 | "trade_type": "FILL", 25 | "price": "0.2577", 26 | "size": "5", 27 | "commission": "0.007731", 28 | "product_id": "ALGO-USD", 29 | "sequence_timestamp": "2023-01-29T05:17:29.024480Z", 30 | "liquidity_indicator": "TAKER", 31 | "size_in_quote": false, 32 | "user_id": "cd5d3286-b5ad-5f48-b356-1fd3e4e73f72", 33 | "side": "BUY" 34 | }, 35 | { 36 | "entry_id": "fb90371cd0539e81a9b3223c344cab3d6556000d8bb0685870852a2963d269b1", 37 | "trade_id": "9777c1fa-4b1b-421f-b1d8-365da2a7355a", 38 | "order_id": "95a50b31-7128-49ac-bba9-0e7200051a92", 39 | "trade_time": "2023-01-28T17:08:46.533Z", 40 | "trade_type": "FILL", 41 | "price": "0.2549", 42 | "size": "5", 43 | "commission": "0.007647", 44 | "product_id": "ALGO-USD", 45 | "sequence_timestamp": "2023-01-28T17:08:46.538118Z", 46 | "liquidity_indicator": "TAKER", 47 | "size_in_quote": false, 48 | "user_id": "cd5d3286-b5ad-5f48-b356-1fd3e4e73f72", 49 | "side": "SELL" 50 | }, 51 | { 52 | "entry_id": "2e95db7b3ff5584bda9b2e928b787e51aec107b88395b7f4885ac2dd988744cd", 53 | "trade_id": "6899d7ea-887a-4477-966a-48c7d8ba3e19", 54 | "order_id": "1f71a67f-6964-4a58-9438-411a5a6f22fc", 55 | "trade_time": "2023-01-28T17:07:27.376Z", 56 | "trade_type": "FILL", 57 | "price": "0.255", 58 | "size": "0.9940357852882704", 59 | "commission": "0.0059642147117296", 60 | "product_id": "ALGO-USD", 61 | "sequence_timestamp": "2023-01-28T17:07:27.382084Z", 62 | "liquidity_indicator": "TAKER", 63 | "size_in_quote": true, 64 | "user_id": "cd5d3286-b5ad-5f48-b356-1fd3e4e73f72", 65 | "side": "BUY" 66 | }, 67 | { 68 | "entry_id": "bcace8280d5a104ca8c5c25efd13738bd0d8f74300da92985a83ccbb5b38ab75", 69 | "trade_id": "33195752-bf2e-483a-b8b8-cf1900c6c153", 70 | "order_id": "f4555646-5f1b-40da-bce6-d2472957c747", 71 | "trade_time": "2023-01-19T16:22:44.572734Z", 72 | "trade_type": "FILL", 73 | "price": "0.2138", 74 | "size": "5", 75 | "commission": "0.006414", 76 | "product_id": "ALGO-USD", 77 | "sequence_timestamp": "2023-01-19T16:22:44.577082Z", 78 | "liquidity_indicator": "TAKER", 79 | "size_in_quote": false, 80 | "user_id": "cd5d3286-b5ad-5f48-b356-1fd3e4e73f72", 81 | "side": "BUY" 82 | } 83 | ], 84 | "cursor": "31971874" 85 | } -------------------------------------------------------------------------------- /tests/fixtures/list_fills_all_call_2_success_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "fills": [ 3 | { 4 | "entry_id": "da7042594c7570bec6094dbc13384ad6e9d9f5cb12c00ddb174f782b01c3fd65", 5 | "trade_id": "37d31b1e-0238-471f-b1e7-1667a8a849e2", 6 | "order_id": "3f9ad0fa-266e-495e-82d0-04616beb402f", 7 | "trade_time": "2023-01-29T05:25:44.826Z", 8 | "trade_type": "FILL", 9 | "price": "0.2569", 10 | "size": "5", 11 | "commission": "0.007707", 12 | "product_id": "ALGO-USD", 13 | "sequence_timestamp": "2023-01-29T05:25:44.829980Z", 14 | "liquidity_indicator": "TAKER", 15 | "size_in_quote": false, 16 | "user_id": "cd5d3286-b5ad-5f48-b356-1fd3e4e73f72", 17 | "side": "SELL" 18 | }, 19 | { 20 | "entry_id": "9cf80aefa90a33cc205465bb2764e7fa1bc3160f35137b16709f084f04755237", 21 | "trade_id": "b3311a1f-9ec2-4e9d-9ca3-96be13b11ae3", 22 | "order_id": "4866b58d-8baf-4996-b5e5-629f0270dd0a", 23 | "trade_time": "2023-01-29T05:17:29.020Z", 24 | "trade_type": "FILL", 25 | "price": "0.2577", 26 | "size": "5", 27 | "commission": "0.007731", 28 | "product_id": "ALGO-USD", 29 | "sequence_timestamp": "2023-01-29T05:17:29.024480Z", 30 | "liquidity_indicator": "TAKER", 31 | "size_in_quote": false, 32 | "user_id": "cd5d3286-b5ad-5f48-b356-1fd3e4e73f72", 33 | "side": "BUY" 34 | }, 35 | { 36 | "entry_id": "fb90371cd0539e81a9b3223c344cab3d6556000d8bb0685870852a2963d269b1", 37 | "trade_id": "9777c1fa-4b1b-421f-b1d8-365da2a7355a", 38 | "order_id": "95a50b31-7128-49ac-bba9-0e7200051a92", 39 | "trade_time": "2023-01-28T17:08:46.533Z", 40 | "trade_type": "FILL", 41 | "price": "0.2549", 42 | "size": "5", 43 | "commission": "0.007647", 44 | "product_id": "ALGO-USD", 45 | "sequence_timestamp": "2023-01-28T17:08:46.538118Z", 46 | "liquidity_indicator": "TAKER", 47 | "size_in_quote": false, 48 | "user_id": "cd5d3286-b5ad-5f48-b356-1fd3e4e73f72", 49 | "side": "SELL" 50 | }, 51 | { 52 | "entry_id": "2e95db7b3ff5584bda9b2e928b787e51aec107b88395b7f4885ac2dd988744cd", 53 | "trade_id": "6899d7ea-887a-4477-966a-48c7d8ba3e19", 54 | "order_id": "1f71a67f-6964-4a58-9438-411a5a6f22fc", 55 | "trade_time": "2023-01-28T17:07:27.376Z", 56 | "trade_type": "FILL", 57 | "price": "0.255", 58 | "size": "0.9940357852882704", 59 | "commission": "0.0059642147117296", 60 | "product_id": "ALGO-USD", 61 | "sequence_timestamp": "2023-01-28T17:07:27.382084Z", 62 | "liquidity_indicator": "TAKER", 63 | "size_in_quote": true, 64 | "user_id": "cd5d3286-b5ad-5f48-b356-1fd3e4e73f72", 65 | "side": "BUY" 66 | }, 67 | { 68 | "entry_id": "bcace8280d5a104ca8c5c25efd13738bd0d8f74300da92985a83ccbb5b38ab75", 69 | "trade_id": "33195752-bf2e-483a-b8b8-cf1900c6c153", 70 | "order_id": "f4555646-5f1b-40da-bce6-d2472957c747", 71 | "trade_time": "2023-01-19T16:22:44.572734Z", 72 | "trade_type": "FILL", 73 | "price": "0.2138", 74 | "size": "5", 75 | "commission": "0.006414", 76 | "product_id": "ALGO-USD", 77 | "sequence_timestamp": "2023-01-19T16:22:44.577082Z", 78 | "liquidity_indicator": "TAKER", 79 | "size_in_quote": false, 80 | "user_id": "cd5d3286-b5ad-5f48-b356-1fd3e4e73f72", 81 | "side": "BUY" 82 | } 83 | ], 84 | "cursor": "" 85 | } -------------------------------------------------------------------------------- /tests/fixtures/list_fills_all_call_1_success_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "fills": [ 3 | { 4 | "entry_id": "da7042594c7570bec6094dbc13384ad6e9d9f5cb12c00ddb174f782b01c3fd65", 5 | "trade_id": "37d31b1e-0238-471f-b1e7-1667a8a849e2", 6 | "order_id": "3f9ad0fa-266e-495e-82d0-04616beb402f", 7 | "trade_time": "2023-01-29T05:25:44.826Z", 8 | "trade_type": "FILL", 9 | "price": "0.2569", 10 | "size": "5", 11 | "commission": "0.007707", 12 | "product_id": "ALGO-USD", 13 | "sequence_timestamp": "2023-01-29T05:25:44.829980Z", 14 | "liquidity_indicator": "TAKER", 15 | "size_in_quote": false, 16 | "user_id": "cd5d3286-b5ad-5f48-b356-1fd3e4e73f72", 17 | "side": "SELL" 18 | }, 19 | { 20 | "entry_id": "9cf80aefa90a33cc205465bb2764e7fa1bc3160f35137b16709f084f04755237", 21 | "trade_id": "b3311a1f-9ec2-4e9d-9ca3-96be13b11ae3", 22 | "order_id": "4866b58d-8baf-4996-b5e5-629f0270dd0a", 23 | "trade_time": "2023-01-29T05:17:29.020Z", 24 | "trade_type": "FILL", 25 | "price": "0.2577", 26 | "size": "5", 27 | "commission": "0.007731", 28 | "product_id": "ALGO-USD", 29 | "sequence_timestamp": "2023-01-29T05:17:29.024480Z", 30 | "liquidity_indicator": "TAKER", 31 | "size_in_quote": false, 32 | "user_id": "cd5d3286-b5ad-5f48-b356-1fd3e4e73f72", 33 | "side": "BUY" 34 | }, 35 | { 36 | "entry_id": "fb90371cd0539e81a9b3223c344cab3d6556000d8bb0685870852a2963d269b1", 37 | "trade_id": "9777c1fa-4b1b-421f-b1d8-365da2a7355a", 38 | "order_id": "95a50b31-7128-49ac-bba9-0e7200051a92", 39 | "trade_time": "2023-01-28T17:08:46.533Z", 40 | "trade_type": "FILL", 41 | "price": "0.2549", 42 | "size": "5", 43 | "commission": "0.007647", 44 | "product_id": "ALGO-USD", 45 | "sequence_timestamp": "2023-01-28T17:08:46.538118Z", 46 | "liquidity_indicator": "TAKER", 47 | "size_in_quote": false, 48 | "user_id": "cd5d3286-b5ad-5f48-b356-1fd3e4e73f72", 49 | "side": "SELL" 50 | }, 51 | { 52 | "entry_id": "2e95db7b3ff5584bda9b2e928b787e51aec107b88395b7f4885ac2dd988744cd", 53 | "trade_id": "6899d7ea-887a-4477-966a-48c7d8ba3e19", 54 | "order_id": "1f71a67f-6964-4a58-9438-411a5a6f22fc", 55 | "trade_time": "2023-01-28T17:07:27.376Z", 56 | "trade_type": "FILL", 57 | "price": "0.255", 58 | "size": "0.9940357852882704", 59 | "commission": "0.0059642147117296", 60 | "product_id": "ALGO-USD", 61 | "sequence_timestamp": "2023-01-28T17:07:27.382084Z", 62 | "liquidity_indicator": "TAKER", 63 | "size_in_quote": true, 64 | "user_id": "cd5d3286-b5ad-5f48-b356-1fd3e4e73f72", 65 | "side": "BUY" 66 | }, 67 | { 68 | "entry_id": "bcace8280d5a104ca8c5c25efd13738bd0d8f74300da92985a83ccbb5b38ab75", 69 | "trade_id": "33195752-bf2e-483a-b8b8-cf1900c6c153", 70 | "order_id": "f4555646-5f1b-40da-bce6-d2472957c747", 71 | "trade_time": "2023-01-19T16:22:44.572734Z", 72 | "trade_type": "FILL", 73 | "price": "0.2138", 74 | "size": "5", 75 | "commission": "0.006414", 76 | "product_id": "ALGO-USD", 77 | "sequence_timestamp": "2023-01-19T16:22:44.577082Z", 78 | "liquidity_indicator": "TAKER", 79 | "size_in_quote": false, 80 | "user_id": "cd5d3286-b5ad-5f48-b356-1fd3e4e73f72", 81 | "side": "BUY" 82 | } 83 | ], 84 | "cursor": "somecursor" 85 | } -------------------------------------------------------------------------------- /coinbaseadvanced/models/fees.py: -------------------------------------------------------------------------------- 1 | """ 2 | Object models for fees related endpoints args and response. 3 | """ 4 | 5 | from typing import Optional 6 | import requests 7 | 8 | from coinbaseadvanced.models.common import BaseModel 9 | from coinbaseadvanced.models.error import CoinbaseAdvancedTradeAPIError 10 | 11 | 12 | class FeeTier(BaseModel): 13 | """ 14 | Fee Tier object. 15 | """ 16 | 17 | pricing_tier: str 18 | usd_from: int 19 | usd_to: str 20 | taker_fee_rate: str 21 | maker_fee_rate: str 22 | 23 | def __init__(self, 24 | pricing_tier: str, 25 | usd_from: int, 26 | usd_to: str, 27 | taker_fee_rate: str, 28 | maker_fee_rate: str, **kwargs) -> None: 29 | self.pricing_tier = pricing_tier 30 | self.usd_from = usd_from 31 | self.usd_to = usd_to 32 | self.taker_fee_rate = taker_fee_rate 33 | self.maker_fee_rate = maker_fee_rate 34 | 35 | self.kwargs = kwargs 36 | 37 | 38 | class GoodsAndServicesTax(BaseModel): 39 | """ 40 | Object representing Goods and Services Tax data. 41 | """ 42 | 43 | rate: str 44 | type: str 45 | 46 | def __init__(self, rate: str, type: str, **kwargs) -> None: 47 | self.rate = rate 48 | self.type = type 49 | 50 | self.kwargs = kwargs 51 | 52 | 53 | class MarginRate(BaseModel): 54 | """ 55 | Margin Rate. 56 | """ 57 | 58 | value: str 59 | 60 | def __init__(self, value: str, **kwargs) -> None: 61 | self.value = value 62 | 63 | self.kwargs = kwargs 64 | 65 | 66 | class TransactionsSummary(BaseModel): 67 | """ 68 | Transactions Summary. 69 | """ 70 | 71 | total_volume: int 72 | total_fees: int 73 | fee_tier: Optional[FeeTier] 74 | margin_rate: Optional[MarginRate] 75 | goods_and_services_tax: Optional[GoodsAndServicesTax] 76 | advanced_trade_only_volume: int 77 | advanced_trade_only_fees: int 78 | coinbase_pro_volume: int 79 | coinbase_pro_fees: int 80 | total_balance: str 81 | has_promo_fee: bool 82 | 83 | def __init__(self, 84 | total_volume: int, 85 | total_fees: int, 86 | fee_tier: dict, 87 | margin_rate: dict, 88 | goods_and_services_tax: dict, 89 | advanced_trade_only_volume: int, 90 | advanced_trade_only_fees: int, 91 | coinbase_pro_volume: int, 92 | coinbase_pro_fees: int, 93 | total_balance: str, 94 | has_promo_fee: bool, **kwargs) -> None: 95 | self.total_volume = total_volume 96 | self.total_fees = total_fees 97 | self.fee_tier = FeeTier(**fee_tier) if fee_tier is not None else None 98 | self.margin_rate = MarginRate( 99 | **margin_rate) if margin_rate is not None else None 100 | self.goods_and_services_tax = GoodsAndServicesTax( 101 | **goods_and_services_tax) if goods_and_services_tax is not None else None 102 | self.advanced_trade_only_volume = advanced_trade_only_volume 103 | self.advanced_trade_only_fees = advanced_trade_only_fees 104 | self.coinbase_pro_volume = coinbase_pro_volume 105 | self.coinbase_pro_fees = coinbase_pro_fees 106 | 107 | self.total_balance = total_balance 108 | self.has_promo_fee = has_promo_fee 109 | 110 | self.kwargs = kwargs 111 | 112 | @classmethod 113 | def from_response(cls, response: requests.Response) -> 'TransactionsSummary': 114 | """ 115 | Factory Method. 116 | """ 117 | 118 | if not response.ok: 119 | raise CoinbaseAdvancedTradeAPIError.not_ok_response(response) 120 | 121 | result = response.json() 122 | return cls(**result) 123 | -------------------------------------------------------------------------------- /tests/fixtures/list_products_success_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "products": [ 3 | { 4 | "product_id": "BTC-USD", 5 | "price": "23148.22", 6 | "price_percentage_change_24h": "1.35253648205659", 7 | "volume_24h": "22695.79392397", 8 | "volume_percentage_change_24h": "-32.67115168820984", 9 | "base_increment": "0.00000001", 10 | "quote_increment": "0.01", 11 | "quote_min_size": "1", 12 | "quote_max_size": "50000000", 13 | "base_min_size": "0.000016", 14 | "base_max_size": "2600", 15 | "base_name": "Bitcoin", 16 | "quote_name": "US Dollar", 17 | "watched": false, 18 | "is_disabled": false, 19 | "new": false, 20 | "status": "online", 21 | "cancel_only": false, 22 | "limit_only": false, 23 | "post_only": false, 24 | "trading_disabled": false, 25 | "auction_mode": false, 26 | "product_type": "SPOT", 27 | "quote_currency_id": "USD", 28 | "base_currency_id": "BTC", 29 | "fcm_trading_session_details": null, 30 | "mid_market_price": "", 31 | "alias": "", 32 | "alias_to": [], 33 | "base_display_symbol": "BTC", 34 | "quote_display_symbol": "USD" 35 | }, 36 | { 37 | "product_id": "ETH-USD", 38 | "price": "1584.27", 39 | "price_percentage_change_24h": "0.89220893355241", 40 | "volume_24h": "246888.78922891", 41 | "volume_percentage_change_24h": "-29.58086454141906", 42 | "base_increment": "0.00000001", 43 | "quote_increment": "0.01", 44 | "quote_min_size": "1", 45 | "quote_max_size": "50000000", 46 | "base_min_size": "0.00022", 47 | "base_max_size": "38000", 48 | "base_name": "Ethereum", 49 | "quote_name": "US Dollar", 50 | "watched": false, 51 | "is_disabled": false, 52 | "new": false, 53 | "status": "online", 54 | "cancel_only": false, 55 | "limit_only": false, 56 | "post_only": false, 57 | "trading_disabled": false, 58 | "auction_mode": false, 59 | "product_type": "SPOT", 60 | "quote_currency_id": "USD", 61 | "base_currency_id": "ETH", 62 | "fcm_trading_session_details": null, 63 | "mid_market_price": "", 64 | "alias": "", 65 | "alias_to": [], 66 | "base_display_symbol": "ETH", 67 | "quote_display_symbol": "USD" 68 | }, 69 | { 70 | "product_id": "DOGE-USD", 71 | "price": "0.09256", 72 | "price_percentage_change_24h": "1.85979971387697", 73 | "volume_24h": "894084193.9", 74 | "volume_percentage_change_24h": "22.95060758325578", 75 | "base_increment": "0.1", 76 | "quote_increment": "0.00001", 77 | "quote_min_size": "1", 78 | "quote_max_size": "10000000", 79 | "base_min_size": "3.8", 80 | "base_max_size": "16000000", 81 | "base_name": "Dogecoin", 82 | "quote_name": "US Dollar", 83 | "watched": false, 84 | "is_disabled": false, 85 | "new": false, 86 | "status": "online", 87 | "cancel_only": false, 88 | "limit_only": false, 89 | "post_only": false, 90 | "trading_disabled": false, 91 | "auction_mode": false, 92 | "product_type": "SPOT", 93 | "quote_currency_id": "USD", 94 | "base_currency_id": "DOGE", 95 | "fcm_trading_session_details": null, 96 | "mid_market_price": "", 97 | "alias": "", 98 | "alias_to": [], 99 | "base_display_symbol": "DOGE", 100 | "quote_display_symbol": "USD" 101 | }, 102 | { 103 | "product_id": "USDT-USD", 104 | "price": "1", 105 | "price_percentage_change_24h": "0.0080006400512", 106 | "volume_24h": "76305193.86", 107 | "volume_percentage_change_24h": "-44.00064832355389", 108 | "base_increment": "0.01", 109 | "quote_increment": "0.00001", 110 | "quote_min_size": "1", 111 | "quote_max_size": "10000000", 112 | "base_min_size": "0.99", 113 | "base_max_size": "9000000", 114 | "base_name": "Tether", 115 | "quote_name": "US Dollar", 116 | "watched": false, 117 | "is_disabled": false, 118 | "new": false, 119 | "status": "online", 120 | "cancel_only": false, 121 | "limit_only": false, 122 | "post_only": false, 123 | "trading_disabled": false, 124 | "auction_mode": false, 125 | "product_type": "SPOT", 126 | "quote_currency_id": "USD", 127 | "base_currency_id": "USDT", 128 | "fcm_trading_session_details": null, 129 | "mid_market_price": "", 130 | "alias": "", 131 | "alias_to": [], 132 | "base_display_symbol": "USDT", 133 | "quote_display_symbol": "USD" 134 | }, 135 | { 136 | "product_id": "SOL-USD", 137 | "price": "23.92", 138 | "price_percentage_change_24h": "-0.33333333333333", 139 | "volume_24h": "1634318.207", 140 | "volume_percentage_change_24h": "-44.43382473170284", 141 | "base_increment": "0.001", 142 | "quote_increment": "0.01", 143 | "quote_min_size": "1", 144 | "quote_max_size": "25000000", 145 | "base_min_size": "0.08", 146 | "base_max_size": "780000", 147 | "base_name": "Solana", 148 | "quote_name": "US Dollar", 149 | "watched": false, 150 | "is_disabled": false, 151 | "new": false, 152 | "status": "online", 153 | "cancel_only": false, 154 | "limit_only": false, 155 | "post_only": false, 156 | "trading_disabled": false, 157 | "auction_mode": false, 158 | "product_type": "SPOT", 159 | "quote_currency_id": "USD", 160 | "base_currency_id": "SOL", 161 | "fcm_trading_session_details": null, 162 | "mid_market_price": "", 163 | "alias": "", 164 | "alias_to": [], 165 | "base_display_symbol": "SOL", 166 | "quote_display_symbol": "USD" 167 | } 168 | ], 169 | "num_products": 5 170 | } -------------------------------------------------------------------------------- /coinbaseadvanced/client_websocket.py: -------------------------------------------------------------------------------- 1 | """ 2 | API Client for Coinbase Websocket channels. 3 | """ 4 | 5 | import json 6 | import time 7 | import threading 8 | import websocket 9 | from coinbaseadvanced.models.market_data import CandlesEvent, HeartbeatEvent, Level2Event, MarketTradesEvent, StatusEvent, TickerBatchEvent, TickerEvent, UserEvent 10 | from coinbaseadvanced.utils import generate_jwt 11 | 12 | # Mapping of channel names to their corresponding event classes. 13 | # https://docs.cdp.coinbase.com/advanced-trade/docs/ws-channels/#heartbeats-channel 14 | CHANNELS = { 15 | 'heartbeat': HeartbeatEvent, # Channel for heartbeat events 16 | 'candles': CandlesEvent, # Real-time updates on product candles 17 | 'market_trades': MarketTradesEvent, # Real-time updates every time a market trade happens 18 | 'status': StatusEvent, # Sends all products and currencies on a preset interval 19 | 'ticker': TickerEvent, # Real-time price updates every time a match happens 20 | 'ticker_batch': TickerBatchEvent, # Real-time price updates every 5000 milliseconds 21 | 'l2_data': Level2Event, # All updates and easiest way to keep order book snapshot 22 | 'user': UserEvent, # Only sends messages that include the authenticated user 23 | } 24 | 25 | 26 | class CoinbaseWebSocketClient: 27 | def __init__(self, api_key: str, signing_key: str, ws_url: str = "wss://advanced-trade-ws.coinbase.com"): 28 | """ 29 | Initializes the CoinbaseWebSocketClient with API key, signing key, and WebSocket URL. 30 | 31 | :param api_key: The API key for Coinbase. 32 | :param signing_key: The signing key for generating JWT. 33 | :param ws_url: The WebSocket URL for connecting to Coinbase. Defaults to the advanced trade WebSocket URL. 34 | """ 35 | self.api_key = api_key 36 | self.signing_key = signing_key 37 | self.ws_url = ws_url 38 | self.callbacks = {} 39 | 40 | def _create_message(self, message_type: str, product_ids: list, channel: str) -> dict: 41 | """ 42 | Creates a subscription message to send to the WebSocket. 43 | 44 | :param message_type: The type of message (e.g., "subscribe"). 45 | :param product_ids: List of product IDs to subscribe to. 46 | :param channel: The channel to subscribe to. 47 | :return: A dictionary containing the subscription message. 48 | """ 49 | jwt_token = generate_jwt(self.api_key, self.signing_key) 50 | return { 51 | "type": message_type, 52 | "product_ids": product_ids, 53 | "channel": channel, 54 | "jwt": jwt_token, 55 | "timestamp": int(time.time()) 56 | } 57 | 58 | def _handle_message(self, ws: websocket.WebSocket, message: str) -> None: 59 | """ 60 | Handles incoming WebSocket messages. 61 | 62 | :param ws: The WebSocket instance. 63 | :param message: The message received from the WebSocket. 64 | :return: The event object created from the message or None if the message type is not recognized. 65 | """ 66 | data = json.loads(message) 67 | 68 | if data.get('type') == 'error': 69 | raise ValueError(f"Error message: {data['message']}") 70 | 71 | if data.get('channel') == 'subscriptions': 72 | return 73 | 74 | if data.get('channel') == 'heartbeats': 75 | heartbeat_event = HeartbeatEvent( 76 | channel=data['channel'], 77 | current_time=data['events'][0]['current_time'], 78 | heartbeat_counter=data['events'][0]['heartbeat_counter'] 79 | ) 80 | if 'heartbeats' in self.callbacks: 81 | self.callbacks['heartbeats'](heartbeat_event) 82 | return 83 | 84 | channel = data.get('channel') 85 | if channel and channel in CHANNELS: 86 | event_class = CHANNELS[channel] 87 | try: 88 | event = event_class(**data) 89 | if channel in self.callbacks: 90 | self.callbacks[channel](event) 91 | return event 92 | except TypeError as e: 93 | raise TypeError(f"Error creating event for channel {channel}: {e}") from e 94 | else: 95 | raise ValueError(f"Unrecognized channel: {channel}") 96 | 97 | def subscribe(self, product_ids: list, channel: str, callback=None): 98 | """ 99 | Subscribes to a specified channel for a list of product IDs and sets a callback for handling messages. 100 | 101 | :param product_ids: List of product IDs to subscribe to. 102 | :param channel: The channel to subscribe to. 103 | :param callback: Optional callback function to handle messages from the channel. 104 | """ 105 | if callback: 106 | self.callbacks[channel] = callback 107 | 108 | def run(): 109 | ws = websocket.WebSocketApp( 110 | self.ws_url, 111 | on_message=self._handle_message, 112 | on_error=self._on_error, 113 | on_close=self._on_close 114 | ) 115 | ws.on_open = lambda ws: self._on_open(ws, product_ids, channel) 116 | ws.run_forever() 117 | 118 | thread = threading.Thread(target=run) 119 | thread.start() 120 | 121 | def _on_open(self, ws: websocket.WebSocket, product_ids: list, channel: str): 122 | """ 123 | Handles the WebSocket connection opening by sending subscription messages. 124 | 125 | :param ws: The WebSocket instance. 126 | :param product_ids: List of product IDs to subscribe to. 127 | :param channel: The channel to subscribe to. 128 | """ 129 | subscribe_message = self._create_message("subscribe", product_ids, channel) 130 | ws.send(json.dumps(subscribe_message)) 131 | 132 | heartbeat_message = self._create_message("subscribe", product_ids, "heartbeats") 133 | ws.send(json.dumps(heartbeat_message)) 134 | 135 | def _on_error(self, ws: websocket.WebSocket, error: str): 136 | """ 137 | Handles errors from the WebSocket. 138 | 139 | :param ws: The WebSocket instance. 140 | :param error: The error message. 141 | """ 142 | raise websocket.WebSocketException(f"WebSocket error: {error}") 143 | 144 | def _on_close(self, ws: websocket.WebSocket, close_status_code, close_msg): 145 | """ 146 | Handles the WebSocket connection closing. 147 | 148 | :param ws: The WebSocket instance. 149 | :param close_status_code: The status code for the connection closure. 150 | :param close_msg: The message for the connection closure. 151 | """ 152 | raise websocket.WebSocketConnectionClosedException( 153 | f"Closed connection with status: {close_status_code}, message: {close_msg}") 154 | -------------------------------------------------------------------------------- /tests/fixtures/get_product_candles_success_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "candles": [ 3 | { 4 | "start": "1675123200", 5 | "low": "0.235", 6 | "high": "0.2441", 7 | "open": "0.2392", 8 | "close": "0.2413", 9 | "volume": "28515145.3" 10 | }, 11 | { 12 | "start": "1675036800", 13 | "low": "0.2353", 14 | "high": "0.2625", 15 | "open": "0.2617", 16 | "close": "0.2391", 17 | "volume": "32451973.7" 18 | }, 19 | { 20 | "start": "1674950400", 21 | "low": "0.2539", 22 | "high": "0.2646", 23 | "open": "0.2573", 24 | "close": "0.2618", 25 | "volume": "28429829.3" 26 | }, 27 | { 28 | "start": "1674864000", 29 | "low": "0.2495", 30 | "high": "0.2646", 31 | "open": "0.2546", 32 | "close": "0.2574", 33 | "volume": "27220277.9" 34 | }, 35 | { 36 | "start": "1674777600", 37 | "low": "0.2383", 38 | "high": "0.2595", 39 | "open": "0.2453", 40 | "close": "0.2545", 41 | "volume": "35761011.2" 42 | }, 43 | { 44 | "start": "1674691200", 45 | "low": "0.2391", 46 | "high": "0.2489", 47 | "open": "0.2448", 48 | "close": "0.2452", 49 | "volume": "26892767.3" 50 | }, 51 | { 52 | "start": "1674604800", 53 | "low": "0.2289", 54 | "high": "0.2491", 55 | "open": "0.2363", 56 | "close": "0.2447", 57 | "volume": "32011531.1" 58 | }, 59 | { 60 | "start": "1674518400", 61 | "low": "0.2336", 62 | "high": "0.26", 63 | "open": "0.2495", 64 | "close": "0.2365", 65 | "volume": "37410222.4" 66 | }, 67 | { 68 | "start": "1674432000", 69 | "low": "0.245", 70 | "high": "0.254", 71 | "open": "0.2458", 72 | "close": "0.2496", 73 | "volume": "33458690" 74 | }, 75 | { 76 | "start": "1674345600", 77 | "low": "0.2352", 78 | "high": "0.2549", 79 | "open": "0.2389", 80 | "close": "0.2458", 81 | "volume": "44696023.8" 82 | }, 83 | { 84 | "start": "1674259200", 85 | "low": "0.2307", 86 | "high": "0.2457", 87 | "open": "0.2392", 88 | "close": "0.2389", 89 | "volume": "46592006.9" 90 | }, 91 | { 92 | "start": "1674172800", 93 | "low": "0.2157", 94 | "high": "0.2402", 95 | "open": "0.2171", 96 | "close": "0.2393", 97 | "volume": "35394140.2" 98 | }, 99 | { 100 | "start": "1674086400", 101 | "low": "0.208", 102 | "high": "0.2186", 103 | "open": "0.2083", 104 | "close": "0.217", 105 | "volume": "26612527.7" 106 | }, 107 | { 108 | "start": "1674000000", 109 | "low": "0.2073", 110 | "high": "0.2326", 111 | "open": "0.2284", 112 | "close": "0.2086", 113 | "volume": "45023915.3" 114 | }, 115 | { 116 | "start": "1673913600", 117 | "low": "0.2276", 118 | "high": "0.2364", 119 | "open": "0.2342", 120 | "close": "0.2284", 121 | "volume": "30450638.3" 122 | }, 123 | { 124 | "start": "1673827200", 125 | "low": "0.2238", 126 | "high": "0.2417", 127 | "open": "0.2404", 128 | "close": "0.2341", 129 | "volume": "50248752.5" 130 | }, 131 | { 132 | "start": "1673740800", 133 | "low": "0.2232", 134 | "high": "0.2407", 135 | "open": "0.2291", 136 | "close": "0.2404", 137 | "volume": "49134595.6" 138 | }, 139 | { 140 | "start": "1673654400", 141 | "low": "0.2091", 142 | "high": "0.2388", 143 | "open": "0.2207", 144 | "close": "0.2291", 145 | "volume": "80854960.9" 146 | }, 147 | { 148 | "start": "1673568000", 149 | "low": "0.2076", 150 | "high": "0.2228", 151 | "open": "0.2123", 152 | "close": "0.2207", 153 | "volume": "32965884" 154 | }, 155 | { 156 | "start": "1673481600", 157 | "low": "0.2032", 158 | "high": "0.2133", 159 | "open": "0.2102", 160 | "close": "0.2123", 161 | "volume": "34980223.4" 162 | }, 163 | { 164 | "start": "1673395200", 165 | "low": "0.1965", 166 | "high": "0.2113", 167 | "open": "0.2038", 168 | "close": "0.2102", 169 | "volume": "25919502.9" 170 | }, 171 | { 172 | "start": "1673308800", 173 | "low": "0.1967", 174 | "high": "0.2055", 175 | "open": "0.1997", 176 | "close": "0.2038", 177 | "volume": "24997626" 178 | }, 179 | { 180 | "start": "1673222400", 181 | "low": "0.1975", 182 | "high": "0.208", 183 | "open": "0.1992", 184 | "close": "0.1996", 185 | "volume": "37025646.9" 186 | }, 187 | { 188 | "start": "1673136000", 189 | "low": "0.1907", 190 | "high": "0.2005", 191 | "open": "0.194", 192 | "close": "0.1993", 193 | "volume": "18141886.6" 194 | }, 195 | { 196 | "start": "1673049600", 197 | "low": "0.1878", 198 | "high": "0.1955", 199 | "open": "0.1891", 200 | "close": "0.1939", 201 | "volume": "18472181.6" 202 | }, 203 | { 204 | "start": "1672963200", 205 | "low": "0.1803", 206 | "high": "0.1899", 207 | "open": "0.1835", 208 | "close": "0.189", 209 | "volume": "18455389.7" 210 | }, 211 | { 212 | "start": "1672876800", 213 | "low": "0.1817", 214 | "high": "0.1909", 215 | "open": "0.1867", 216 | "close": "0.1836", 217 | "volume": "22991519.9" 218 | }, 219 | { 220 | "start": "1672790400", 221 | "low": "0.1833", 222 | "high": "0.1916", 223 | "open": "0.1841", 224 | "close": "0.1868", 225 | "volume": "32034899.4" 226 | }, 227 | { 228 | "start": "1672704000", 229 | "low": "0.1798", 230 | "high": "0.1852", 231 | "open": "0.1808", 232 | "close": "0.1841", 233 | "volume": "16887066.6" 234 | }, 235 | { 236 | "start": "1672617600", 237 | "low": "0.173", 238 | "high": "0.183", 239 | "open": "0.1773", 240 | "close": "0.1807", 241 | "volume": "28378286" 242 | } 243 | ] 244 | } -------------------------------------------------------------------------------- /tests/playground.py: -------------------------------------------------------------------------------- 1 | import json 2 | import random 3 | import string 4 | 5 | from dotenv import load_dotenv 6 | import os 7 | 8 | from datetime import datetime, timezone 9 | 10 | from coinbaseadvanced.client import CoinbaseAdvancedTradeAPIClient, Side, StopDirection, Granularity 11 | from coinbaseadvanced.models.common import ValueCurrency 12 | 13 | from tests.fixtures.fixtures import * 14 | 15 | load_dotenv() # Load environment variables from a .env file 16 | 17 | # Cloud API Trading Keys (NEW/Recommended): https://cloud.coinbase.com/access/api 18 | # API_KEY_NAME = os.getenv('API_KEY_NAME') 19 | # PRIVATE_KEY = os.getenv('PRIVATE_KEY').replace('\\n', '\n') 20 | API_KEY_NAME = os.getenv('API_KEY_NAME_FULL') 21 | PRIVATE_KEY = os.getenv('PRIVATE_KEY_FULL').replace('\\n', '\n') 22 | 23 | # Legacy API Keys: https://www.coinbase.com/settings/api 24 | API_KEY = os.getenv('API_KEY') 25 | SECRET_KEY = os.getenv('SECRET_KEY') 26 | 27 | # A real Account Id (ALGO WALLET ID, BTC WALLET ID, or ETH WALLET ID, etc...) 28 | ACCOUNT_ID = os.getenv('ACCOUNT_ID') 29 | 30 | 31 | def generate_random_id(): 32 | return ''.join(random.choice(string.ascii_letters + string.digits) for i in range(20)) 33 | 34 | 35 | def audit(func, args, fixture_obj): 36 | print(f"endpoint: {func.__name__}") 37 | result_obj = func() if args is None else func(**args) 38 | 39 | # Checking if Coinbase is returning more items that expected. 40 | if result_obj.kwargs: 41 | print(f" - Response => Object: NEED UPDATE") 42 | 43 | # Checking fixtures files are updated. 44 | result_obj_atttributes = [ 45 | attr for attr, v in result_obj.__dict__.items() 46 | if v is not None and not attr.startswith('__') and not callable(attr) and attr != 'kwargs'] 47 | for k in result_obj_atttributes: 48 | if not k in fixture_obj: 49 | print( 50 | f" - Response => Fixtures: NEED UPDATE, key '{k}' present in live response but not found in fixture.") 51 | 52 | 53 | # Creating the client per authentication methods. 54 | # client = CoinbaseAdvancedTradeAPIClient.from_legacy_api_keys(API_KEY, SECRET_KEY) 55 | client = CoinbaseAdvancedTradeAPIClient.from_cloud_api_keys( 56 | API_KEY_NAME, PRIVATE_KEY) 57 | print() 58 | 59 | # Accounts 60 | 61 | # audit(client.list_accounts, None, json.loads(fixture_list_accounts_success_response().text)) 62 | # audit(client.list_accounts_all, None, json.loads(fixture_list_accounts_all_call_1_success_response().text)) 63 | # audit(client.get_account, {'account_id': ACCOUNT_ID}, 64 | # json.loads(fixture_get_account_success_response().text)['account']) 65 | 66 | # Orders 67 | 68 | # Check and Verify here: https://www.coinbase.com/orders) 69 | # when running this section. 70 | 71 | # Unccoment below for runnig the post function which are usually commented 72 | 73 | # fixture_obj = json.loads(fixture_create_limit_order_success_response().text) 74 | # fixture_obj['success_response']['order_configuration'] = fixture_obj['order_configuration'] 75 | # fixture_obj = fixture_obj['success_response'] 76 | # audit(client.create_limit_order, { 77 | # 'client_order_id': generate_random_id(), 78 | # 'product_id': "ALGO-USD", 79 | # 'side': Side.BUY, 80 | # 'limit_price': ".12", 81 | # 'base_size': 10, 82 | # 'retail_portfolio_id': "bba559eb-5b24-45f5-9898-472a81c46a56" 83 | # }, 84 | # fixture_obj 85 | # ) 86 | 87 | # audit(client.edit_order, { 88 | # 'order_id': "754eb3d3-13ad-4f25-b239-c79257b49f10", 89 | # 'limit_price': 0.08, 90 | # 'base_size': 6 91 | # }, json.loads( 92 | # fixture_edit_order_success_response().text)) 93 | 94 | audit(client.edit_order_preview, { 95 | 'order_id': "754eb3d3-13ad-4f25-b239-c79257b49f10", 96 | 'limit_price': 0.08, 97 | 'base_size': 6 98 | }, json.loads( 99 | fixture_edit_order_preview_success_response().text)) 100 | 101 | # audit(client.cancel_orders, {'order_ids': ["42a266d3-591b-43d4-a968-a9a126f7b1a5", "82c6919f-6884-4127-95af-11db89b21ed3", 102 | # "c1d5ab66-d99a-4329-9c1d-be6a9f32c686"]}, json.loads(fixture_cancel_orders_success_response().text)) 103 | 104 | # audit(client.list_orders, {'start_date': datetime(2023, 1, 25), 105 | # 'end_date': datetime(2023, 1, 30), 106 | # 'limit': 10}, json.loads(fixture_list_orders_success_response().text)) 107 | 108 | # audit(client.list_orders_all, {'start_date': datetime(2023, 1, 25), 109 | # 'end_date': datetime(2023, 1, 30), 110 | # 'limit': 10}, json.loads(fixture_list_orders_all_call_1_success_response().text)) 111 | 112 | # audit(client.list_fills, {'start_date': datetime(2023, 1, 25), 113 | # 'end_date': datetime(2023, 1, 30), 114 | # 'limit': 10}, json.loads(fixture_list_fills_success_response().text)) 115 | 116 | # audit(client.list_fills_all, {'start_date': datetime(2023, 1, 25), 117 | # 'end_date': datetime(2023, 1, 30), 118 | # 'limit': 10}, json.loads(fixture_list_fills_all_call_1_success_response().text)) 119 | 120 | # audit(client.get_order, {'order_id': 'c1d5ab66-d99a-4329-9c1d-be6a9f32c686'}, 121 | # json.loads(fixture_get_order_success_response().text)['order']) 122 | 123 | # Products 124 | 125 | # audit(client.list_products, {'limit': 5}, 126 | # json.loads(fixture_list_products_success_response().text)) 127 | 128 | # audit(client.get_product_candles, { 129 | # 'product_id': "ALGO-USD", 130 | # 'start_date': datetime(2023, 1, 1, tzinfo=timezone.utc), 131 | # 'end_date': datetime(2023, 1, 31, tzinfo=timezone.utc), 132 | # 'granularity': Granularity.ONE_DAY}, 133 | # json.loads(fixture_get_product_candles_success_response().text)) 134 | 135 | # audit(client.get_product_candles_all, { 136 | # 'product_id': "ALGO-USD", 137 | # 'start_date': datetime(2023, 1, 1, tzinfo=timezone.utc), 138 | # 'end_date': datetime(2023, 1, 31, tzinfo=timezone.utc), 139 | # 'granularity': Granularity.ONE_DAY}, 140 | # json.loads(fixture_get_product_candles_all_call_1_success_response().text)) 141 | 142 | # audit(client.get_market_trades, { 143 | # 'product_id': "ALGO-USD", 144 | # 'limit': 10}, 145 | # json.loads(fixture_get_trades_success_response().text)) 146 | 147 | # audit(client.get_best_bid_ask, {"product_ids": ["BTC-USD", "ETH-USD"]}, 148 | # json.loads(fixture_get_best_bid_asks_success_response().text)) 149 | 150 | # audit(client.get_product_book, {"product_id": "BTC-USD", "limit": 5}, 151 | # json.loads(fixture_product_book_success_response().text)) 152 | 153 | # Fees 154 | 155 | # audit(client.get_transactions_summary, { 156 | # 'start_date': datetime(2023, 1, 1, tzinfo=timezone.utc), 157 | # 'end_date': datetime(2023, 1, 31, tzinfo=timezone.utc)}, 158 | # json.loads(fixture_get_transactions_summary_success_response().text)) 159 | 160 | # Portfolios 161 | 162 | # audit(client.list_portfolios, {}, 163 | # json.loads(fixture_list_portfolios_success_response().text)) 164 | 165 | # audit(client.create_portfolio, {'name': 'test-portfolio-name-2'}, 166 | # json.loads(fixture_create_portfolio_success_response().text)['portfolio']) 167 | 168 | # audit(client.edit_portfolio, {'portfolio_uuid': '354808f3-06df-42d7-87ec-488f34ff6f14', 'name': 'test-edit-portfolio-name'}, 169 | # json.loads(fixture_create_portfolio_success_response().text)['portfolio']) 170 | 171 | # EXPECTED: "Response => Fixtures: NEED UPDATE, key 'success' present in live response but not found in fixture." 172 | # audit(client.delete_portfolio, {'portfolio_uuid': 'a78767c7-6d83-4c0c-a736-7f70ef866324'}, 173 | # json.loads(fixture_delete_portfolio_success_response().text)) 174 | 175 | # audit(client.get_portfolio_breakdown, {'portfolio_uuid': 'e7ae4c9c-fd97-46c9-a6e4-5048893b5dc3'}, 176 | # json.loads(fixture_get_portfolio_breakdown_success_response().text)['breakdown']) 177 | 178 | # audit(client.move_portfolio_funds, {'funds_value': '0.1', 'funds_currency': 'USD', 'source_portfolio_uuid': 'bba559eb-5b24-45f5-9898-472a81c46a56', 'target_portfolio_uuid': 'e7ae4c9c-fd97-46c9-a6e4-5048893b5dc3'}, 179 | # json.loads(fixture_move_funds_success_response().text)) 180 | 181 | # Common 182 | # audit(client.get_unix_time, {}, 183 | # json.loads(fixture_get_unix_time_success_response().text)) 184 | -------------------------------------------------------------------------------- /coinbaseadvanced/models/portfolios.py: -------------------------------------------------------------------------------- 1 | """ 2 | Object models for portfolios related endpoints args and response. 3 | """ 4 | 5 | from typing import List 6 | from enum import Enum 7 | from uuid import UUID 8 | 9 | import requests 10 | 11 | from coinbaseadvanced.models.common import BaseModel, ValueCurrency 12 | from coinbaseadvanced.models.error import CoinbaseAdvancedTradeAPIError 13 | from coinbaseadvanced.models.futures import FuturesPosition, FuturesPositionSide, MarginType 14 | 15 | 16 | class UserRawCurrency(BaseModel): 17 | """ 18 | Represents a user's raw currency. 19 | 20 | Attributes: 21 | user_native_currency (ValueCurrency): The user's native currency. 22 | raw_currency (ValueCurrency): The raw currency. 23 | """ 24 | 25 | user_native_currency: ValueCurrency 26 | raw_currency: ValueCurrency 27 | 28 | def __init__(self, userNativeCurrency: dict, rawCurrency: dict): 29 | self.user_native_currency = ValueCurrency(**userNativeCurrency) 30 | self.raw_currency = ValueCurrency(**rawCurrency) 31 | 32 | 33 | class PortfolioType(Enum): 34 | """ 35 | Enum representing whether "BUY" or "SELL" order. 36 | """ 37 | 38 | UNDEFINED = "UNDEFINED" 39 | DEFAULT = "DEFAULT" 40 | CONSUMER = "CONSUMER" 41 | INTX = "INTX" 42 | 43 | 44 | class Portfolio(BaseModel): 45 | """ 46 | Object representing a portfolio. 47 | """ 48 | 49 | uuid: UUID 50 | name: str 51 | type: PortfolioType 52 | deleted: bool 53 | 54 | def __init__( 55 | self, uuid: UUID, name: str, type: str, deleted: bool, **kwargs) -> None: 56 | self.uuid = uuid 57 | self.name = name 58 | self.type = PortfolioType[type] 59 | self.deleted = deleted 60 | 61 | self.kwargs = kwargs 62 | 63 | @classmethod 64 | def from_response(cls, response: requests.Response) -> 'Portfolio': 65 | """ 66 | Factory Method. 67 | """ 68 | 69 | if not response.ok: 70 | raise CoinbaseAdvancedTradeAPIError.not_ok_response(response) 71 | 72 | result = response.json() 73 | return cls(**result['portfolio']) 74 | 75 | 76 | class PortfolioBalances(BaseModel): 77 | """ 78 | Object representing a portfolio balances. 79 | """ 80 | 81 | total_balance: ValueCurrency 82 | total_futures_balance: ValueCurrency 83 | total_cash_equivalent_balance: ValueCurrency 84 | total_crypto_balance: ValueCurrency 85 | futures_unrealized_pnl: ValueCurrency 86 | perp_unrealized_pnl: ValueCurrency 87 | 88 | def __init__( 89 | self, total_balance: dict, 90 | total_futures_balance: dict, 91 | total_cash_equivalent_balance: dict, 92 | total_crypto_balance: dict, 93 | futures_unrealized_pnl: dict, 94 | perp_unrealized_pnl: dict, **kwargs) -> None: 95 | self.total_balance = ValueCurrency(**total_balance) 96 | self.total_futures_balance = ValueCurrency(**total_futures_balance) 97 | self.total_cash_equivalent_balance = ValueCurrency( 98 | **total_cash_equivalent_balance) 99 | self.total_crypto_balance = ValueCurrency(**total_crypto_balance) 100 | self.futures_unrealized_pnl = ValueCurrency( 101 | **futures_unrealized_pnl) 102 | self.perp_unrealized_pnl = ValueCurrency(**perp_unrealized_pnl) 103 | 104 | self.kwargs = kwargs 105 | 106 | 107 | class SpotPosition(BaseModel): 108 | """ 109 | Object representing a spot position. 110 | """ 111 | 112 | asset: str 113 | account_uuid: str 114 | total_balance_fiat: float 115 | total_balance_crypto: float 116 | available_to_trade_fiat: float 117 | allocation: float 118 | one_day_change: float 119 | cost_basis: ValueCurrency 120 | asset_img_url: str 121 | is_cash: bool 122 | 123 | def __init__( 124 | self, asset: str, 125 | account_uuid: str, 126 | total_balance_fiat: float, 127 | total_balance_crypto: float, 128 | available_to_trade_fiat: float, 129 | allocation: float, 130 | one_day_change: float, 131 | cost_basis: dict, 132 | asset_img_url: str, 133 | is_cash: bool, **kwargs) -> None: 134 | 135 | self.asset = asset 136 | self.account_uuid = account_uuid 137 | self.total_balance_fiat = total_balance_fiat 138 | self.total_balance_crypto = total_balance_crypto 139 | self.available_to_trade_fiat = available_to_trade_fiat 140 | self.allocation = allocation 141 | self.one_day_change = one_day_change 142 | self.cost_basis = ValueCurrency(**cost_basis) 143 | self.asset_img_url = asset_img_url 144 | self.is_cash = is_cash 145 | 146 | self.kwargs = kwargs 147 | 148 | 149 | class PerpPosition(BaseModel): 150 | """ 151 | Object representing a perp position. 152 | """ 153 | 154 | product_id: str 155 | product_uuid: str 156 | symbol: str 157 | asset_image_url: str 158 | vwap: UserRawCurrency 159 | position_side: FuturesPositionSide 160 | net_size: str 161 | buy_order_size: str 162 | sell_order_size: str 163 | im_contribution: str 164 | unrealized_pnl: UserRawCurrency 165 | mark_price: UserRawCurrency 166 | liquidation_price: UserRawCurrency 167 | leverage: str 168 | im_notional: UserRawCurrency 169 | mm_notional: UserRawCurrency 170 | position_notional: UserRawCurrency 171 | margin_type: MarginType 172 | liquidation_buffer: str 173 | liquidation_percentage: str 174 | 175 | def __init__( 176 | self, 177 | product_id: str, 178 | product_uuid: str, 179 | symbol: str, 180 | asset_image_url: str, 181 | vwap: dict, 182 | position_side: str, 183 | net_size: str, 184 | buy_order_size: str, 185 | sell_order_size: str, 186 | im_contribution: str, 187 | unrealized_pnl: dict, 188 | mark_price: dict, 189 | liquidation_price: dict, 190 | leverage: str, 191 | im_notional: dict, 192 | mm_notional: dict, 193 | position_notional: dict, 194 | margin_type: str, 195 | liquidation_buffer: str, 196 | liquidation_percentage: str, **kwargs) -> None: 197 | 198 | self.product_id = product_id 199 | self.product_uuid = product_uuid 200 | self.symbol = symbol 201 | self.asset_image_url = asset_image_url 202 | self.vwap = UserRawCurrency(**vwap) 203 | self.position_side = FuturesPositionSide[position_side] 204 | self.net_size = net_size 205 | self.buy_order_size = buy_order_size 206 | self.sell_order_size = sell_order_size 207 | self.im_contribution = im_contribution 208 | self.unrealized_pnl = UserRawCurrency(**unrealized_pnl) 209 | self.mark_price = UserRawCurrency(**mark_price) 210 | self.liquidation_price = UserRawCurrency(**liquidation_price) 211 | self.leverage = leverage 212 | self.im_notional = UserRawCurrency(**im_notional) 213 | self.mm_notional = UserRawCurrency(**mm_notional) 214 | self.position_notional = UserRawCurrency(**position_notional) 215 | self.margin_type = MarginType[margin_type] 216 | self.liquidation_buffer = liquidation_buffer 217 | self.liquidation_percentage = liquidation_percentage 218 | 219 | self.kwargs = kwargs 220 | 221 | 222 | class PortfolioBreakdown(BaseModel): 223 | """ 224 | Object representing a portfolio breakdown. 225 | """ 226 | 227 | portfolio: Portfolio 228 | portfolio_balances: PortfolioBalances 229 | spot_positions: List[SpotPosition] 230 | perp_positions: List[PerpPosition] 231 | futures_positions: List[FuturesPosition] 232 | 233 | def __init__(self, portfolio: dict, 234 | portfolio_balances: dict, 235 | spot_positions: list, 236 | perp_positions: list, 237 | futures_positions: list, **kwargs): 238 | self.portfolio = Portfolio(**portfolio) 239 | self.portfolio_balances = PortfolioBalances(**portfolio_balances) 240 | self.spot_positions = [SpotPosition(**x) for x in spot_positions] 241 | self.perp_positions = [PerpPosition(**x) for x in perp_positions] 242 | self.futures_positions = [ 243 | FuturesPosition(**x) for x in futures_positions] 244 | 245 | self.kwargs = kwargs 246 | 247 | @classmethod 248 | def from_response(cls, response: requests.Response) -> 'PortfolioBreakdown': 249 | """ 250 | Factory Method. 251 | """ 252 | 253 | if not response.ok: 254 | raise CoinbaseAdvancedTradeAPIError.not_ok_response(response) 255 | 256 | result = response.json() 257 | return cls(**result['breakdown']) 258 | 259 | 260 | class PortfoliosPage(BaseModel): 261 | """ 262 | Portfolio Page. 263 | """ 264 | 265 | portfolios: List[Portfolio] 266 | 267 | def __init__(self, portfolios: list, **kwargs) -> None: 268 | self.portfolios = list(map(lambda x: Portfolio(**x), portfolios)) \ 269 | if portfolios is not None else [] 270 | 271 | self.kwargs = kwargs 272 | 273 | @classmethod 274 | def from_response(cls, response: requests.Response) -> 'PortfoliosPage': 275 | """ 276 | Factory Method. 277 | """ 278 | 279 | if not response.ok: 280 | raise CoinbaseAdvancedTradeAPIError.not_ok_response(response) 281 | 282 | result = response.json() 283 | return cls(**result) 284 | 285 | def __iter__(self): 286 | return self.portfolios.__iter__() 287 | 288 | 289 | class PortfolioFundsTransfer(BaseModel): 290 | """ 291 | Represents a funds transfer between two portfolios. 292 | """ 293 | 294 | source_portfolio_uuid: str 295 | target_portfolio_uuid: str 296 | 297 | def __init__(self, source_portfolio_uuid: str, target_portfolio_uuid: str, **kwargs): 298 | """ 299 | Initializes a new instance of the PortfolioFundsTransfer class. 300 | 301 | Args: 302 | source_portfolio_uuid (str): The UUID of the source portfolio. 303 | target_portfolio_uuid (str): The UUID of the target portfolio. 304 | **kwargs: Additional keyword arguments. 305 | 306 | """ 307 | self.source_portfolio_uuid = source_portfolio_uuid 308 | self.target_portfolio_uuid = target_portfolio_uuid 309 | self.kwargs = kwargs 310 | 311 | @classmethod 312 | def from_response(cls, response: requests.Response) -> 'PortfolioFundsTransfer': 313 | """ 314 | Factory method that creates a PortfolioFundsTransfer object from a response. 315 | 316 | Args: 317 | response (requests.Response): The response object. 318 | 319 | Returns: 320 | PortfolioFundsTransfer: The created PortfolioFundsTransfer object. 321 | 322 | Raises: 323 | CoinbaseAdvancedTradeAPIError: If the response is not OK. 324 | 325 | """ 326 | if not response.ok: 327 | raise CoinbaseAdvancedTradeAPIError.not_ok_response(response) 328 | 329 | result = response.json() 330 | return cls(**result) 331 | -------------------------------------------------------------------------------- /coinbaseadvanced/models/products.py: -------------------------------------------------------------------------------- 1 | """ 2 | Object models for products related endpoints args and response. 3 | """ 4 | 5 | from uuid import UUID 6 | from datetime import datetime 7 | from typing import List 8 | from enum import Enum 9 | 10 | import requests 11 | 12 | from coinbaseadvanced.models.common import BaseModel 13 | from coinbaseadvanced.models.error import CoinbaseAdvancedTradeAPIError 14 | 15 | 16 | class ProductType(Enum): 17 | """ 18 | Enum representing different product types. 19 | """ 20 | 21 | SPOT = "SPOT" 22 | 23 | 24 | GRANULARITY_MAP_IN_MINUTES = { 25 | "ONE_MINUTE": 1, 26 | "FIVE_MINUTE": 5, 27 | "FIFTEEN_MINUTE": 15, 28 | "THIRTY_MINUTE": 30, 29 | "ONE_HOUR": 60, 30 | "TWO_HOUR": 120, 31 | "SIX_HOUR": 360, 32 | "ONE_DAY": 1440, 33 | } 34 | 35 | 36 | class Granularity(Enum): 37 | """ 38 | Enum representing time range for product candles. 39 | """ 40 | 41 | UNKNOWN = "UNKNOWN_GRANULARITY" 42 | ONE_MINUTE = "ONE_MINUTE" 43 | FIVE_MINUTE = "FIVE_MINUTE" 44 | FIFTEEN_MINUTE = "FIFTEEN_MINUTE" 45 | THIRTY_MINUTE = "THIRTY_MINUTE" 46 | ONE_HOUR = "ONE_HOUR" 47 | TWO_HOUR = "TWO_HOUR" 48 | SIX_HOUR = "SIX_HOUR" 49 | ONE_DAY = "ONE_DAY" 50 | 51 | 52 | class Product(BaseModel): 53 | """ 54 | Object representing a product. 55 | """ 56 | 57 | product_id: str 58 | price: str 59 | price_percentage_change_24h: str 60 | volume_24h: int 61 | volume_percentage_change_24h: str 62 | base_increment: str 63 | quote_increment: str 64 | quote_min_size: str 65 | quote_max_size: int 66 | base_min_size: str 67 | base_max_size: int 68 | base_name: str 69 | quote_name: str 70 | watched: bool 71 | is_disabled: bool 72 | new: bool 73 | status: str 74 | cancel_only: bool 75 | limit_only: bool 76 | post_only: bool 77 | trading_disabled: bool 78 | auction_mode: bool 79 | product_type: str 80 | quote_currency_id: str 81 | base_currency_id: str 82 | mid_market_price: str 83 | fcm_trading_session_details: str 84 | alias: str 85 | alias_to: list 86 | base_display_symbol: str 87 | quote_display_symbol: str 88 | 89 | def __init__(self, 90 | product_id: str, 91 | price: str, 92 | price_percentage_change_24h: str, 93 | volume_24h: int, 94 | volume_percentage_change_24h: str, 95 | base_increment: str, 96 | quote_increment: str, 97 | quote_min_size: str, 98 | quote_max_size: int, 99 | base_min_size: str, 100 | base_max_size: int, 101 | base_name: str, 102 | quote_name: str, 103 | watched: bool, 104 | is_disabled: bool, 105 | new: bool, 106 | status: str, 107 | cancel_only: bool, 108 | limit_only: bool, 109 | post_only: bool, 110 | trading_disabled: bool, 111 | auction_mode: bool, 112 | product_type: str, 113 | quote_currency_id: str, 114 | base_currency_id: str, 115 | mid_market_price: str, 116 | fcm_trading_session_details: str, 117 | alias: str, 118 | alias_to: list, 119 | base_display_symbol: str, 120 | quote_display_symbol: str, **kwargs 121 | ) -> None: 122 | self.product_id = product_id 123 | self.price = price 124 | self.price_percentage_change_24h = price_percentage_change_24h 125 | self.volume_24h = volume_24h 126 | self.volume_percentage_change_24h = volume_percentage_change_24h 127 | self.base_increment = base_increment 128 | self.quote_increment = quote_increment 129 | self.quote_min_size = quote_min_size 130 | self.quote_max_size = quote_max_size 131 | self.base_min_size = base_min_size 132 | self.base_max_size = base_max_size 133 | self.base_name = base_name 134 | self.quote_name = quote_name 135 | self.watched = watched 136 | self.is_disabled = is_disabled 137 | self.new = new 138 | self.status = status 139 | self.cancel_only = cancel_only 140 | self.limit_only = limit_only 141 | self.post_only = post_only 142 | self.trading_disabled = trading_disabled 143 | self.auction_mode = auction_mode 144 | self.product_type = product_type 145 | self.quote_currency_id = quote_currency_id 146 | self.base_currency_id = base_currency_id 147 | self.mid_market_price = mid_market_price 148 | self.fcm_trading_session_details = fcm_trading_session_details 149 | self.alias = alias 150 | self.alias_to = alias_to 151 | self.base_display_symbol = base_display_symbol 152 | self.quote_display_symbol = quote_display_symbol 153 | 154 | self.kwargs = kwargs 155 | 156 | @classmethod 157 | def from_response(cls, response: requests.Response) -> 'Product': 158 | """ 159 | Factory Method. 160 | """ 161 | 162 | if not response.ok: 163 | raise CoinbaseAdvancedTradeAPIError.not_ok_response(response) 164 | 165 | result = response.json() 166 | product_dict = result 167 | return cls(**product_dict) 168 | 169 | 170 | class ProductsPage(BaseModel): 171 | """ 172 | Products Page. 173 | """ 174 | 175 | products: List[Product] 176 | num_products: int 177 | 178 | def __init__(self, products: list, num_products: int, **kwargs) -> None: 179 | self.products = list(map(lambda x: Product(**x), products)) \ 180 | if products is not None else [] 181 | 182 | self.num_products = num_products 183 | 184 | self.kwargs = kwargs 185 | 186 | @classmethod 187 | def from_response(cls, response: requests.Response) -> 'ProductsPage': 188 | """ 189 | Factory Method. 190 | """ 191 | 192 | if not response.ok: 193 | raise CoinbaseAdvancedTradeAPIError.not_ok_response(response) 194 | 195 | result = response.json() 196 | return cls(**result) 197 | 198 | def __iter__(self): 199 | return self.products.__iter__() 200 | 201 | 202 | class Candle(BaseModel): 203 | """ 204 | Candle object. 205 | """ 206 | 207 | start: str 208 | low: str 209 | high: str 210 | open: str 211 | close: str 212 | volume: int 213 | 214 | def __init__(self, 215 | start: str, 216 | low: str, 217 | high: str, 218 | open: str, 219 | close: str, 220 | volume: int, **kwargs) -> None: 221 | self.start = start 222 | self.low = low 223 | self.high = high 224 | self.open = open 225 | self.close = close 226 | self.volume = volume 227 | 228 | self.kwargs = kwargs 229 | 230 | 231 | class CandlesPage(BaseModel): 232 | """ 233 | Page of product candles. 234 | """ 235 | 236 | candles: List[Candle] 237 | 238 | def __init__(self, candles: list, **kwargs) -> None: 239 | self.candles = list(map(lambda x: Candle(**x), candles) 240 | ) if candles is not None else [] 241 | 242 | self.kwargs = kwargs 243 | 244 | @classmethod 245 | def from_response(cls, response: requests.Response) -> 'CandlesPage': 246 | """ 247 | Factory Method. 248 | """ 249 | 250 | if not response.ok: 251 | raise CoinbaseAdvancedTradeAPIError.not_ok_response(response) 252 | 253 | result = response.json() 254 | return cls(**result) 255 | 256 | def __iter__(self): 257 | return self.candles.__iter__() 258 | 259 | 260 | class Bid(BaseModel): 261 | """ 262 | Represents a bid in a trading market. 263 | 264 | Attributes: 265 | price (str): The price of the bid. 266 | size (str): The size of the bid. 267 | """ 268 | 269 | price: str 270 | size: str 271 | 272 | def __init__(self, price: str, size: str, **kwargs): 273 | self.price = price 274 | self.size = size 275 | 276 | self.kwargs = kwargs 277 | 278 | 279 | class Ask(BaseModel): 280 | """ 281 | Represents an ask order in a trading market. 282 | 283 | Attributes: 284 | price (str): The price of the ask order. 285 | size (str): The size of the ask order. 286 | """ 287 | 288 | price: str 289 | size: str 290 | 291 | def __init__(self, price: str, size: str, **kwargs): 292 | self.price = price 293 | self.size = size 294 | 295 | self.kwargs = kwargs 296 | 297 | 298 | class BidAsk(BaseModel): 299 | """ 300 | BidAsk object. 301 | """ 302 | 303 | product_id: str 304 | bids: List[Bid] 305 | asks: List[Ask] 306 | time: str 307 | 308 | def __init__(self, product_id: str, bids: list, asks: list, time: str, **kwargs) -> None: 309 | self.product_id = product_id 310 | self.bids = list(map(lambda x: Bid(**x), bids) 311 | ) if bids is not None else [] 312 | self.asks = list(map(lambda x: Ask(**x), asks) 313 | ) if asks is not None else [] 314 | self.time = time 315 | 316 | self.kwargs = kwargs 317 | 318 | 319 | class BidAsksPage(BaseModel): 320 | """ 321 | Page of bid/asks for products. 322 | """ 323 | 324 | pricebooks: List 325 | 326 | def __init__(self, pricebooks: list, **kwargs) -> None: 327 | self.pricebooks = list(map(lambda x: BidAsk( 328 | **x), pricebooks)) if pricebooks is not None else [] 329 | 330 | self.kwargs = kwargs 331 | 332 | @classmethod 333 | def from_response(cls, response: requests.Response) -> 'BidAsksPage': 334 | """ 335 | Factory Method. 336 | """ 337 | 338 | if not response.ok: 339 | raise CoinbaseAdvancedTradeAPIError.not_ok_response(response) 340 | 341 | result = response.json() 342 | return cls(**result) 343 | 344 | def __iter__(self): 345 | return self.pricebooks.__iter__() 346 | 347 | 348 | class ProductBook(BaseModel): 349 | """ 350 | Product bid/asks. 351 | """ 352 | 353 | pricebook: BidAsk 354 | 355 | def __init__(self, pricebook: dict, **kwargs) -> None: 356 | self.pricebook = BidAsk(**pricebook) 357 | 358 | self.kwargs = kwargs 359 | 360 | @classmethod 361 | def from_response(cls, response: requests.Response) -> 'ProductBook': 362 | """ 363 | Factory Method. 364 | """ 365 | 366 | if not response.ok: 367 | raise CoinbaseAdvancedTradeAPIError.not_ok_response(response) 368 | 369 | result = response.json() 370 | return cls(**result) 371 | 372 | 373 | class Trade(BaseModel): 374 | """ 375 | Trade object data. 376 | """ 377 | 378 | trade_id: UUID 379 | product_id: str 380 | price: str 381 | size: int 382 | time: datetime 383 | side: str 384 | bid: str 385 | ask: str 386 | 387 | def __init__(self, 388 | trade_id: UUID, 389 | product_id: str, 390 | price: str, 391 | size: int, 392 | time: datetime, 393 | side: str, 394 | bid: str, 395 | ask: str, **kwargs) -> None: 396 | self.trade_id = trade_id 397 | self.product_id = product_id 398 | self.price = price 399 | self.size = size 400 | self.time = time 401 | self.side = side 402 | self.bid = bid 403 | self.ask = ask 404 | 405 | self.kwargs = kwargs 406 | 407 | 408 | class TradesPage(BaseModel): 409 | """ 410 | Page of trades. 411 | """ 412 | 413 | trades: List[Trade] 414 | best_bid: str 415 | best_ask: str 416 | 417 | def __init__(self, 418 | trades: list, 419 | best_bid: str, 420 | best_ask: str, **kwargs 421 | ) -> None: 422 | self.trades = list(map(lambda x: Trade(**x), trades) 423 | ) if trades is not None else [] 424 | self.best_bid = best_bid 425 | self.best_ask = best_ask 426 | 427 | self.kwargs = kwargs 428 | 429 | @classmethod 430 | def from_response(cls, response: requests.Response) -> 'TradesPage': 431 | """ 432 | Factory Method. 433 | """ 434 | 435 | if not response.ok: 436 | raise CoinbaseAdvancedTradeAPIError.not_ok_response(response) 437 | 438 | result = response.json() 439 | return cls(**result) 440 | 441 | def __iter__(self): 442 | return self.trades.__iter__() 443 | -------------------------------------------------------------------------------- /tests/fixtures/fixtures.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | import json 4 | 5 | 6 | def _fixtured_mock_response( 7 | ok: bool, 8 | text: str) -> mock.Mock: 9 | """ 10 | since we typically test a bunch of different 11 | requests calls for a service, we are going to do 12 | a lot of mock responses, so its usually a good idea 13 | to have a helper function that builds these things 14 | """ 15 | mock_resp = mock.Mock() 16 | # set status code and content 17 | mock_resp.ok = ok 18 | mock_resp.text = text 19 | mock_resp.json.return_value = json.loads(text) 20 | 21 | return mock_resp 22 | 23 | 24 | def fixture_get_account_success_response() -> mock.Mock: 25 | with open('tests/fixtures/get_account_success_response.json', 'r', encoding="utf-8") as file: 26 | content = file.read() 27 | return _fixtured_mock_response( 28 | ok=True, 29 | text=content) 30 | 31 | 32 | def fixture_default_failure_response() -> mock.Mock: 33 | with open('tests/fixtures/default_failure_response.json', 'r', encoding="utf-8") as file: 34 | content = file.read() 35 | return _fixtured_mock_response( 36 | ok=False, 37 | text=content) 38 | 39 | 40 | def fixture_list_accounts_success_response() -> mock.Mock: 41 | with open('tests/fixtures/list_accounts_success_response.json', 'r', encoding="utf-8") as file: 42 | content = file.read() 43 | return _fixtured_mock_response( 44 | ok=True, 45 | text=content) 46 | 47 | 48 | def fixture_list_accounts_all_call_1_success_response() -> mock.Mock: 49 | with open('tests/fixtures/list_accounts_all_call_1_success_response.json', 'r', encoding="utf-8") as file: 50 | content = file.read() 51 | return _fixtured_mock_response( 52 | ok=True, 53 | text=content) 54 | 55 | 56 | def fixture_list_accounts_all_call_2_success_response() -> mock.Mock: 57 | with open('tests/fixtures/list_accounts_all_call_2_success_response.json', 'r', encoding="utf-8") as file: 58 | content = file.read() 59 | return _fixtured_mock_response( 60 | ok=True, 61 | text=content) 62 | 63 | 64 | def fixture_create_limit_order_success_response() -> mock.Mock: 65 | with open('tests/fixtures/create_limit_order_success_response.json', 'r', encoding="utf-8") as file: 66 | content = file.read() 67 | return _fixtured_mock_response( 68 | ok=True, 69 | text=content) 70 | 71 | 72 | def fixture_create_stop_limit_order_success_response() -> mock.Mock: 73 | with open('tests/fixtures/create_stop_limit_order_success_response.json', 'r', encoding="utf-8") as file: 74 | content = file.read() 75 | return _fixtured_mock_response( 76 | ok=True, 77 | text=content) 78 | 79 | 80 | def fixture_create_buy_market_order_success_response() -> mock.Mock: 81 | with open('tests/fixtures/create_buy_market_order_success_response.json', 'r', encoding="utf-8") as file: 82 | content = file.read() 83 | return _fixtured_mock_response( 84 | ok=True, 85 | text=content) 86 | 87 | 88 | def fixture_create_sell_market_order_success_response() -> mock.Mock: 89 | with open('tests/fixtures/create_sell_market_order_success_response.json', 'r', encoding="utf-8") as file: 90 | content = file.read() 91 | return _fixtured_mock_response( 92 | ok=True, 93 | text=content) 94 | 95 | 96 | def fixture_edit_order_success_response() -> mock.Mock: 97 | with open('tests/fixtures/edit_order_success_response.json', 'r', encoding="utf-8") as file: 98 | content = file.read() 99 | return _fixtured_mock_response( 100 | ok=True, 101 | text=content) 102 | 103 | 104 | def fixture_edit_order_preview_success_response() -> mock.Mock: 105 | with open('tests/fixtures/edit_order_preview_success_response.json', 'r', encoding="utf-8") as file: 106 | content = file.read() 107 | return _fixtured_mock_response( 108 | ok=True, 109 | text=content) 110 | 111 | 112 | def fixture_default_order_failure_response() -> mock.Mock: 113 | with open('tests/fixtures/default_order_failure_response.json', 'r', encoding="utf-8") as file: 114 | content = file.read() 115 | return _fixtured_mock_response( 116 | ok=False, 117 | text=content) 118 | 119 | 120 | def fixture_order_failure_no_funds_response() -> mock.Mock: 121 | with open('tests/fixtures/create_order_failure_no_funds_response.json', 'r', encoding="utf-8") as file: 122 | content = file.read() 123 | return _fixtured_mock_response( 124 | ok=True, 125 | text=content) 126 | 127 | 128 | def fixture_cancel_orders_success_response() -> mock.Mock: 129 | with open('tests/fixtures/cancel_orders_success_response.json', 'r', encoding="utf-8") as file: 130 | content = file.read() 131 | return _fixtured_mock_response( 132 | ok=True, 133 | text=content) 134 | 135 | 136 | def fixture_list_orders_success_response() -> mock.Mock: 137 | with open('tests/fixtures/list_orders_success_response.json', 'r', encoding="utf-8") as file: 138 | content = file.read() 139 | return _fixtured_mock_response( 140 | ok=True, 141 | text=content) 142 | 143 | 144 | def fixture_list_orders_all_call_1_success_response() -> mock.Mock: 145 | with open('tests/fixtures/list_orders_all_call_1_success_response.json', 'r', encoding="utf-8") as file: 146 | content = file.read() 147 | return _fixtured_mock_response( 148 | ok=True, 149 | text=content) 150 | 151 | 152 | def fixture_list_orders_all_call_2_success_response() -> mock.Mock: 153 | with open('tests/fixtures/list_orders_all_call_2_success_response.json', 'r', encoding="utf-8") as file: 154 | content = file.read() 155 | return _fixtured_mock_response( 156 | ok=True, 157 | text=content) 158 | 159 | 160 | def fixture_list_orders_with_extra_unnamed_success_response() -> mock.Mock: 161 | with open('tests/fixtures/list_orders_with_extra_unnamed_success_response.json', 'r', encoding="utf-8") as file: 162 | content = file.read() 163 | return _fixtured_mock_response( 164 | ok=True, 165 | text=content) 166 | 167 | 168 | def fixture_list_fills_success_response() -> mock.Mock: 169 | with open('tests/fixtures/list_fills_success_response.json', 'r', encoding="utf-8") as file: 170 | content = file.read() 171 | return _fixtured_mock_response( 172 | ok=True, 173 | text=content) 174 | 175 | 176 | def fixture_list_fills_all_call_1_success_response() -> mock.Mock: 177 | with open('tests/fixtures/list_fills_all_call_1_success_response.json', 'r', encoding="utf-8") as file: 178 | content = file.read() 179 | return _fixtured_mock_response( 180 | ok=True, 181 | text=content) 182 | 183 | 184 | def fixture_list_fills_all_call_2_success_response() -> mock.Mock: 185 | with open('tests/fixtures/list_fills_all_call_2_success_response.json', 'r', encoding="utf-8") as file: 186 | content = file.read() 187 | return _fixtured_mock_response( 188 | ok=True, 189 | text=content) 190 | 191 | 192 | def fixture_get_order_success_response() -> mock.Mock: 193 | with open('tests/fixtures/get_order_success_response.json', 'r', encoding="utf-8") as file: 194 | content = file.read() 195 | return _fixtured_mock_response( 196 | ok=True, 197 | text=content) 198 | 199 | 200 | def fixture_list_products_success_response() -> mock.Mock: 201 | with open('tests/fixtures/list_products_success_response.json', 'r', encoding="utf-8") as file: 202 | content = file.read() 203 | return _fixtured_mock_response( 204 | ok=True, 205 | text=content) 206 | 207 | 208 | def fixture_get_product_success_response() -> mock.Mock: 209 | with open('tests/fixtures/get_product_success_response.json', 'r', encoding="utf-8") as file: 210 | content = file.read() 211 | return _fixtured_mock_response( 212 | ok=True, 213 | text=content) 214 | 215 | 216 | def fixture_get_product_candles_success_response() -> mock.Mock: 217 | with open('tests/fixtures/get_product_candles_success_response.json', 'r', encoding="utf-8") as file: 218 | content = file.read() 219 | return _fixtured_mock_response( 220 | ok=True, 221 | text=content) 222 | 223 | 224 | def fixture_get_product_candles_all_call_1_success_response() -> mock.Mock: 225 | with open('tests/fixtures/get_product_candles_all_call_1_success_response.json', 'r', encoding="utf-8") as file: 226 | content = file.read() 227 | return _fixtured_mock_response( 228 | ok=True, 229 | text=content) 230 | 231 | 232 | def fixture_get_product_candles_all_call_2_success_response() -> mock.Mock: 233 | with open('tests/fixtures/get_product_candles_all_call_2_success_response.json', 'r', encoding="utf-8") as file: 234 | content = file.read() 235 | return _fixtured_mock_response( 236 | ok=True, 237 | text=content) 238 | 239 | 240 | def fixture_get_product_candles_all_call_3_success_response() -> mock.Mock: 241 | with open('tests/fixtures/get_product_candles_all_call_3_success_response.json', 'r', encoding="utf-8") as file: 242 | content = file.read() 243 | return _fixtured_mock_response( 244 | ok=True, 245 | text=content) 246 | 247 | 248 | def fixture_get_best_bid_asks_success_response() -> mock.Mock: 249 | with open('tests/fixtures/get_best_bid_ask_success_response.json', 'r', encoding="utf-8") as file: 250 | content = file.read() 251 | return _fixtured_mock_response( 252 | ok=True, 253 | text=content) 254 | 255 | 256 | def fixture_product_book_success_response() -> mock.Mock: 257 | with open('tests/fixtures/get_product_book_success_response.json', 'r', encoding="utf-8") as file: 258 | content = file.read() 259 | return _fixtured_mock_response( 260 | ok=True, 261 | text=content) 262 | 263 | 264 | def fixture_get_trades_success_response() -> mock.Mock: 265 | with open('tests/fixtures/get_trades_success_response.json', 'r', encoding="utf-8") as file: 266 | content = file.read() 267 | return _fixtured_mock_response( 268 | ok=True, 269 | text=content) 270 | 271 | 272 | def fixture_get_transactions_summary_success_response() -> mock.Mock: 273 | with open('tests/fixtures/get_transactions_summary_success_response.json', 'r', encoding="utf-8") as file: 274 | content = file.read() 275 | return _fixtured_mock_response( 276 | ok=True, 277 | text=content) 278 | 279 | 280 | def fixture_get_unix_time_success_response() -> mock.Mock: 281 | with open('tests/fixtures/get_unix_time_success_response.json', 'r', encoding="utf-8") as file: 282 | content = file.read() 283 | return _fixtured_mock_response( 284 | ok=True, 285 | text=content) 286 | 287 | 288 | def fixture_list_portfolios_success_response() -> mock.Mock: 289 | with open('tests/fixtures/list_portfolios_success_response.json', 'r', encoding="utf-8") as file: 290 | content = file.read() 291 | return _fixtured_mock_response( 292 | ok=True, 293 | text=content) 294 | 295 | 296 | def fixture_create_portfolio_success_response() -> mock.Mock: 297 | with open('tests/fixtures/create_portfolio_success_response.json', 'r', encoding="utf-8") as file: 298 | content = file.read() 299 | return _fixtured_mock_response( 300 | ok=True, 301 | text=content) 302 | 303 | 304 | def fixture_edit_portfolio_success_response() -> mock.Mock: 305 | with open('tests/fixtures/edit_portfolio_success_response.json', 'r', encoding="utf-8") as file: 306 | content = file.read() 307 | return _fixtured_mock_response( 308 | ok=True, 309 | text=content) 310 | 311 | 312 | def fixture_delete_portfolio_success_response() -> mock.Mock: 313 | with open('tests/fixtures/delete_portfolio_success_response.json', 'r', encoding="utf-8") as file: 314 | content = file.read() 315 | return _fixtured_mock_response( 316 | ok=True, 317 | text=content) 318 | 319 | 320 | def fixture_get_portfolio_breakdown_success_response() -> mock.Mock: 321 | with open('tests/fixtures/get_portfolio_breakdown_success_response.json', 'r', encoding="utf-8") as file: 322 | content = file.read() 323 | return _fixtured_mock_response( 324 | ok=True, 325 | text=content) 326 | 327 | 328 | def fixture_move_funds_success_response() -> mock.Mock: 329 | with open('tests/fixtures/move_funds_success_response.json', 'r', encoding="utf-8") as file: 330 | content = file.read() 331 | return _fixtured_mock_response( 332 | ok=True, 333 | text=content) 334 | -------------------------------------------------------------------------------- /tests/fixtures/list_orders_all_call_2_success_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "orders": [ 3 | { 4 | "order_id": "0a89e021-92f9-49a8-8657-d1a948c72e01", 5 | "product_id": "ALGO-USD", 6 | "user_id": "cd5d3286-b5ad-5f48-b356-1fd3e4e73f72", 7 | "order_configuration": { 8 | "limit_limit_gtc": { 9 | "base_size": "5", 10 | "limit_price": "0.9", 11 | "post_only": false 12 | } 13 | }, 14 | "side": "SELL", 15 | "client_order_id": "j876789ks", 16 | "status": "CANCELLED", 17 | "time_in_force": "GOOD_UNTIL_CANCELLED", 18 | "created_time": "2023-01-29T06:36:23.487310Z", 19 | "completion_percentage": "0", 20 | "filled_size": "0", 21 | "average_filled_price": "0", 22 | "fee": "", 23 | "number_of_fills": "0", 24 | "filled_value": "0", 25 | "pending_cancel": false, 26 | "size_in_quote": false, 27 | "total_fees": "0", 28 | "size_inclusive_of_fees": false, 29 | "total_value_after_fees": "0", 30 | "trigger_status": "INVALID_ORDER_TYPE", 31 | "order_type": "LIMIT", 32 | "reject_reason": "REJECT_REASON_UNSPECIFIED", 33 | "settled": false, 34 | "product_type": "SPOT", 35 | "reject_message": "", 36 | "cancel_message": "User requested cancel" 37 | }, 38 | { 39 | "order_id": "5f96405c-2246-445f-8eb8-839a63c1fe71", 40 | "product_id": "ALGO-USD", 41 | "user_id": "cd5d3286-b5ad-5f48-b356-1fd3e4e73f72", 42 | "order_configuration": { 43 | "limit_limit_gtc": { 44 | "base_size": "5", 45 | "limit_price": "0.9", 46 | "post_only": false 47 | } 48 | }, 49 | "side": "SELL", 50 | "client_order_id": "k7999902", 51 | "status": "CANCELLED", 52 | "time_in_force": "GOOD_UNTIL_CANCELLED", 53 | "created_time": "2023-01-29T06:36:23.275412Z", 54 | "completion_percentage": "0", 55 | "filled_size": "0", 56 | "average_filled_price": "0", 57 | "fee": "", 58 | "number_of_fills": "0", 59 | "filled_value": "0", 60 | "pending_cancel": false, 61 | "size_in_quote": false, 62 | "total_fees": "0", 63 | "size_inclusive_of_fees": false, 64 | "total_value_after_fees": "0", 65 | "trigger_status": "INVALID_ORDER_TYPE", 66 | "order_type": "LIMIT", 67 | "reject_reason": "REJECT_REASON_UNSPECIFIED", 68 | "settled": false, 69 | "product_type": "SPOT", 70 | "reject_message": "", 71 | "cancel_message": "User requested cancel" 72 | }, 73 | { 74 | "order_id": "4258a7d6-fbf9-4519-85c5-0a3707b8ee99", 75 | "product_id": "ALGO-USD", 76 | "user_id": "cd5d3286-b5ad-5f48-b356-1fd3e4e73f72", 77 | "order_configuration": { 78 | "limit_limit_gtc": { 79 | "base_size": "5", 80 | "limit_price": "0.9", 81 | "post_only": false 82 | } 83 | }, 84 | "side": "SELL", 85 | "client_order_id": "jks", 86 | "status": "CANCELLED", 87 | "time_in_force": "GOOD_UNTIL_CANCELLED", 88 | "created_time": "2023-01-29T06:34:09.957562Z", 89 | "completion_percentage": "0", 90 | "filled_size": "0", 91 | "average_filled_price": "0", 92 | "fee": "", 93 | "number_of_fills": "0", 94 | "filled_value": "0", 95 | "pending_cancel": false, 96 | "size_in_quote": false, 97 | "total_fees": "0", 98 | "size_inclusive_of_fees": false, 99 | "total_value_after_fees": "0", 100 | "trigger_status": "INVALID_ORDER_TYPE", 101 | "order_type": "LIMIT", 102 | "reject_reason": "REJECT_REASON_UNSPECIFIED", 103 | "settled": false, 104 | "product_type": "SPOT", 105 | "reject_message": "", 106 | "cancel_message": "User requested cancel" 107 | }, 108 | { 109 | "order_id": "dd87277f-ef6f-40c1-8c27-27238a7c1370", 110 | "product_id": "ALGO-USD", 111 | "user_id": "cd5d3286-b5ad-5f48-b356-1fd3e4e73f72", 112 | "order_configuration": { 113 | "limit_limit_gtc": { 114 | "base_size": "5", 115 | "limit_price": "0.9", 116 | "post_only": false 117 | } 118 | }, 119 | "side": "SELL", 120 | "client_order_id": "k72", 121 | "status": "CANCELLED", 122 | "time_in_force": "GOOD_UNTIL_CANCELLED", 123 | "created_time": "2023-01-29T06:34:09.682464Z", 124 | "completion_percentage": "0", 125 | "filled_size": "0", 126 | "average_filled_price": "0", 127 | "fee": "", 128 | "number_of_fills": "0", 129 | "filled_value": "0", 130 | "pending_cancel": false, 131 | "size_in_quote": false, 132 | "total_fees": "0", 133 | "size_inclusive_of_fees": false, 134 | "total_value_after_fees": "0", 135 | "trigger_status": "INVALID_ORDER_TYPE", 136 | "order_type": "LIMIT", 137 | "reject_reason": "REJECT_REASON_UNSPECIFIED", 138 | "settled": false, 139 | "product_type": "SPOT", 140 | "reject_message": "", 141 | "cancel_message": "User requested cancel" 142 | }, 143 | { 144 | "order_id": "2ab5a57e-f3e4-41fa-bf82-65e7d89640c0", 145 | "product_id": "ALGO-USD", 146 | "user_id": "cd5d3286-b5ad-5f48-b356-1fd3e4e73f72", 147 | "order_configuration": { 148 | "limit_limit_gtc": { 149 | "base_size": "5", 150 | "limit_price": "0.9", 151 | "post_only": false 152 | } 153 | }, 154 | "side": "SELL", 155 | "client_order_id": "jksd89n90s8df-900s8df", 156 | "status": "CANCELLED", 157 | "time_in_force": "GOOD_UNTIL_CANCELLED", 158 | "created_time": "2023-01-29T06:30:21.564102Z", 159 | "completion_percentage": "0", 160 | "filled_size": "0", 161 | "average_filled_price": "0", 162 | "fee": "", 163 | "number_of_fills": "0", 164 | "filled_value": "0", 165 | "pending_cancel": false, 166 | "size_in_quote": false, 167 | "total_fees": "0", 168 | "size_inclusive_of_fees": false, 169 | "total_value_after_fees": "0", 170 | "trigger_status": "INVALID_ORDER_TYPE", 171 | "order_type": "LIMIT", 172 | "reject_reason": "REJECT_REASON_UNSPECIFIED", 173 | "settled": false, 174 | "product_type": "SPOT", 175 | "reject_message": "", 176 | "cancel_message": "User requested cancel" 177 | }, 178 | { 179 | "order_id": "dc98ccb7-8494-418d-99e9-651b76ca64f2", 180 | "product_id": "ALGO-USD", 181 | "user_id": "cd5d3286-b5ad-5f48-b356-1fd3e4e73f72", 182 | "order_configuration": { 183 | "limit_limit_gtc": { 184 | "base_size": "5", 185 | "limit_price": "0.9", 186 | "post_only": false 187 | } 188 | }, 189 | "side": "SELL", 190 | "client_order_id": "k7234k-mop0987klas-9098--asasa", 191 | "status": "CANCELLED", 192 | "time_in_force": "GOOD_UNTIL_CANCELLED", 193 | "created_time": "2023-01-29T06:26:14.759800Z", 194 | "completion_percentage": "0", 195 | "filled_size": "0", 196 | "average_filled_price": "0", 197 | "fee": "", 198 | "number_of_fills": "0", 199 | "filled_value": "0", 200 | "pending_cancel": false, 201 | "size_in_quote": false, 202 | "total_fees": "0", 203 | "size_inclusive_of_fees": false, 204 | "total_value_after_fees": "0", 205 | "trigger_status": "INVALID_ORDER_TYPE", 206 | "order_type": "LIMIT", 207 | "reject_reason": "REJECT_REASON_UNSPECIFIED", 208 | "settled": false, 209 | "product_type": "SPOT", 210 | "reject_message": "", 211 | "cancel_message": "User requested cancel" 212 | }, 213 | { 214 | "order_id": "7abd16cf-0117-4d00-9a66-af158365c2db", 215 | "product_id": "ALGO-USD", 216 | "user_id": "cd5d3286-b5ad-5f48-b356-1fd3e4e73f72", 217 | "order_configuration": { 218 | "limit_limit_gtc": { 219 | "base_size": "5", 220 | "limit_price": "0.9", 221 | "post_only": false 222 | } 223 | }, 224 | "side": "SELL", 225 | "client_order_id": "k7234k-mklas-9098--asasa", 226 | "status": "CANCELLED", 227 | "time_in_force": "GOOD_UNTIL_CANCELLED", 228 | "created_time": "2023-01-29T06:10:03.260178Z", 229 | "completion_percentage": "0", 230 | "filled_size": "0", 231 | "average_filled_price": "0", 232 | "fee": "", 233 | "number_of_fills": "0", 234 | "filled_value": "0", 235 | "pending_cancel": false, 236 | "size_in_quote": false, 237 | "total_fees": "0", 238 | "size_inclusive_of_fees": false, 239 | "total_value_after_fees": "0", 240 | "trigger_status": "INVALID_ORDER_TYPE", 241 | "order_type": "LIMIT", 242 | "reject_reason": "REJECT_REASON_UNSPECIFIED", 243 | "settled": false, 244 | "product_type": "SPOT", 245 | "reject_message": "", 246 | "cancel_message": "User requested cancel" 247 | }, 248 | { 249 | "order_id": "700128ed-a968-4345-b646-ebd8c167d0ec", 250 | "product_id": "ALGO-USD", 251 | "user_id": "cd5d3286-b5ad-5f48-b356-1fd3e4e73f72", 252 | "order_configuration": { 253 | "limit_limit_gtc": { 254 | "base_size": "5", 255 | "limit_price": "0.9", 256 | "post_only": false 257 | } 258 | }, 259 | "side": "SELL", 260 | "client_order_id": "k7234k-mklas--asasa", 261 | "status": "CANCELLED", 262 | "time_in_force": "GOOD_UNTIL_CANCELLED", 263 | "created_time": "2023-01-29T06:08:32.452578Z", 264 | "completion_percentage": "0", 265 | "filled_size": "0", 266 | "average_filled_price": "0", 267 | "fee": "", 268 | "number_of_fills": "0", 269 | "filled_value": "0", 270 | "pending_cancel": false, 271 | "size_in_quote": false, 272 | "total_fees": "0", 273 | "size_inclusive_of_fees": false, 274 | "total_value_after_fees": "0", 275 | "trigger_status": "INVALID_ORDER_TYPE", 276 | "order_type": "LIMIT", 277 | "reject_reason": "REJECT_REASON_UNSPECIFIED", 278 | "settled": false, 279 | "product_type": "SPOT", 280 | "reject_message": "", 281 | "cancel_message": "User requested cancel" 282 | }, 283 | { 284 | "order_id": "1f4e4db8-b9b1-4fed-a597-de404039a07b", 285 | "product_id": "ALGO-USD", 286 | "user_id": "cd5d3286-b5ad-5f48-b356-1fd3e4e73f72", 287 | "order_configuration": { 288 | "limit_limit_gtc": { 289 | "base_size": "5", 290 | "limit_price": "0.9", 291 | "post_only": false 292 | } 293 | }, 294 | "side": "SELL", 295 | "client_order_id": "k7234k-mklas-asa", 296 | "status": "CANCELLED", 297 | "time_in_force": "GOOD_UNTIL_CANCELLED", 298 | "created_time": "2023-01-29T06:05:39.923240Z", 299 | "completion_percentage": "0", 300 | "filled_size": "0", 301 | "average_filled_price": "0", 302 | "fee": "", 303 | "number_of_fills": "0", 304 | "filled_value": "0", 305 | "pending_cancel": false, 306 | "size_in_quote": false, 307 | "total_fees": "0", 308 | "size_inclusive_of_fees": false, 309 | "total_value_after_fees": "0", 310 | "trigger_status": "INVALID_ORDER_TYPE", 311 | "order_type": "LIMIT", 312 | "reject_reason": "REJECT_REASON_UNSPECIFIED", 313 | "settled": false, 314 | "product_type": "SPOT", 315 | "reject_message": "", 316 | "cancel_message": "User requested cancel" 317 | }, 318 | { 319 | "order_id": "e389f2c9-208c-435c-a1c8-b430948e531b", 320 | "product_id": "ALGO-USD", 321 | "user_id": "cd5d3286-b5ad-5f48-b356-1fd3e4e73f72", 322 | "order_configuration": { 323 | "limit_limit_gtc": { 324 | "base_size": "5", 325 | "limit_price": "0.9", 326 | "post_only": false 327 | } 328 | }, 329 | "side": "SELL", 330 | "client_order_id": "k7234k-jkashd879a", 331 | "status": "CANCELLED", 332 | "time_in_force": "GOOD_UNTIL_CANCELLED", 333 | "created_time": "2023-01-29T06:04:21.946256Z", 334 | "completion_percentage": "0", 335 | "filled_size": "0", 336 | "average_filled_price": "0", 337 | "fee": "", 338 | "number_of_fills": "0", 339 | "filled_value": "0", 340 | "pending_cancel": false, 341 | "size_in_quote": false, 342 | "total_fees": "0", 343 | "size_inclusive_of_fees": false, 344 | "total_value_after_fees": "0", 345 | "trigger_status": "INVALID_ORDER_TYPE", 346 | "order_type": "LIMIT", 347 | "reject_reason": "REJECT_REASON_UNSPECIFIED", 348 | "settled": false, 349 | "product_type": "SPOT", 350 | "reject_message": "", 351 | "cancel_message": "User requested cancel" 352 | } 353 | ], 354 | "sequence": "0", 355 | "has_next": false, 356 | "cursor": "" 357 | } -------------------------------------------------------------------------------- /tests/fixtures/list_orders_all_call_1_success_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "orders": [ 3 | { 4 | "order_id": "0a89e021-92f9-49a8-8657-d1a948c72e01", 5 | "product_id": "ALGO-USD", 6 | "user_id": "cd5d3286-b5ad-5f48-b356-1fd3e4e73f72", 7 | "order_configuration": { 8 | "limit_limit_gtc": { 9 | "base_size": "5", 10 | "limit_price": "0.9", 11 | "post_only": false 12 | } 13 | }, 14 | "side": "SELL", 15 | "client_order_id": "j876789ks", 16 | "status": "CANCELLED", 17 | "time_in_force": "GOOD_UNTIL_CANCELLED", 18 | "created_time": "2023-01-29T06:36:23.487310Z", 19 | "completion_percentage": "0", 20 | "filled_size": "0", 21 | "average_filled_price": "0", 22 | "fee": "", 23 | "number_of_fills": "0", 24 | "filled_value": "0", 25 | "pending_cancel": false, 26 | "size_in_quote": false, 27 | "total_fees": "0", 28 | "size_inclusive_of_fees": false, 29 | "total_value_after_fees": "0", 30 | "trigger_status": "INVALID_ORDER_TYPE", 31 | "order_type": "LIMIT", 32 | "reject_reason": "REJECT_REASON_UNSPECIFIED", 33 | "settled": false, 34 | "product_type": "SPOT", 35 | "reject_message": "", 36 | "cancel_message": "User requested cancel" 37 | }, 38 | { 39 | "order_id": "5f96405c-2246-445f-8eb8-839a63c1fe71", 40 | "product_id": "ALGO-USD", 41 | "user_id": "cd5d3286-b5ad-5f48-b356-1fd3e4e73f72", 42 | "order_configuration": { 43 | "limit_limit_gtc": { 44 | "base_size": "5", 45 | "limit_price": "0.9", 46 | "post_only": false 47 | } 48 | }, 49 | "side": "SELL", 50 | "client_order_id": "k7999902", 51 | "status": "CANCELLED", 52 | "time_in_force": "GOOD_UNTIL_CANCELLED", 53 | "created_time": "2023-01-29T06:36:23.275412Z", 54 | "completion_percentage": "0", 55 | "filled_size": "0", 56 | "average_filled_price": "0", 57 | "fee": "", 58 | "number_of_fills": "0", 59 | "filled_value": "0", 60 | "pending_cancel": false, 61 | "size_in_quote": false, 62 | "total_fees": "0", 63 | "size_inclusive_of_fees": false, 64 | "total_value_after_fees": "0", 65 | "trigger_status": "INVALID_ORDER_TYPE", 66 | "order_type": "LIMIT", 67 | "reject_reason": "REJECT_REASON_UNSPECIFIED", 68 | "settled": false, 69 | "product_type": "SPOT", 70 | "reject_message": "", 71 | "cancel_message": "User requested cancel" 72 | }, 73 | { 74 | "order_id": "4258a7d6-fbf9-4519-85c5-0a3707b8ee99", 75 | "product_id": "ALGO-USD", 76 | "user_id": "cd5d3286-b5ad-5f48-b356-1fd3e4e73f72", 77 | "order_configuration": { 78 | "limit_limit_gtc": { 79 | "base_size": "5", 80 | "limit_price": "0.9", 81 | "post_only": false 82 | } 83 | }, 84 | "side": "SELL", 85 | "client_order_id": "jks", 86 | "status": "CANCELLED", 87 | "time_in_force": "GOOD_UNTIL_CANCELLED", 88 | "created_time": "2023-01-29T06:34:09.957562Z", 89 | "completion_percentage": "0", 90 | "filled_size": "0", 91 | "average_filled_price": "0", 92 | "fee": "", 93 | "number_of_fills": "0", 94 | "filled_value": "0", 95 | "pending_cancel": false, 96 | "size_in_quote": false, 97 | "total_fees": "0", 98 | "size_inclusive_of_fees": false, 99 | "total_value_after_fees": "0", 100 | "trigger_status": "INVALID_ORDER_TYPE", 101 | "order_type": "LIMIT", 102 | "reject_reason": "REJECT_REASON_UNSPECIFIED", 103 | "settled": false, 104 | "product_type": "SPOT", 105 | "reject_message": "", 106 | "cancel_message": "User requested cancel" 107 | }, 108 | { 109 | "order_id": "dd87277f-ef6f-40c1-8c27-27238a7c1370", 110 | "product_id": "ALGO-USD", 111 | "user_id": "cd5d3286-b5ad-5f48-b356-1fd3e4e73f72", 112 | "order_configuration": { 113 | "limit_limit_gtc": { 114 | "base_size": "5", 115 | "limit_price": "0.9", 116 | "post_only": false 117 | } 118 | }, 119 | "side": "SELL", 120 | "client_order_id": "k72", 121 | "status": "CANCELLED", 122 | "time_in_force": "GOOD_UNTIL_CANCELLED", 123 | "created_time": "2023-01-29T06:34:09.682464Z", 124 | "completion_percentage": "0", 125 | "filled_size": "0", 126 | "average_filled_price": "0", 127 | "fee": "", 128 | "number_of_fills": "0", 129 | "filled_value": "0", 130 | "pending_cancel": false, 131 | "size_in_quote": false, 132 | "total_fees": "0", 133 | "size_inclusive_of_fees": false, 134 | "total_value_after_fees": "0", 135 | "trigger_status": "INVALID_ORDER_TYPE", 136 | "order_type": "LIMIT", 137 | "reject_reason": "REJECT_REASON_UNSPECIFIED", 138 | "settled": false, 139 | "product_type": "SPOT", 140 | "reject_message": "", 141 | "cancel_message": "User requested cancel" 142 | }, 143 | { 144 | "order_id": "2ab5a57e-f3e4-41fa-bf82-65e7d89640c0", 145 | "product_id": "ALGO-USD", 146 | "user_id": "cd5d3286-b5ad-5f48-b356-1fd3e4e73f72", 147 | "order_configuration": { 148 | "limit_limit_gtc": { 149 | "base_size": "5", 150 | "limit_price": "0.9", 151 | "post_only": false 152 | } 153 | }, 154 | "side": "SELL", 155 | "client_order_id": "jksd89n90s8df-900s8df", 156 | "status": "CANCELLED", 157 | "time_in_force": "GOOD_UNTIL_CANCELLED", 158 | "created_time": "2023-01-29T06:30:21.564102Z", 159 | "completion_percentage": "0", 160 | "filled_size": "0", 161 | "average_filled_price": "0", 162 | "fee": "", 163 | "number_of_fills": "0", 164 | "filled_value": "0", 165 | "pending_cancel": false, 166 | "size_in_quote": false, 167 | "total_fees": "0", 168 | "size_inclusive_of_fees": false, 169 | "total_value_after_fees": "0", 170 | "trigger_status": "INVALID_ORDER_TYPE", 171 | "order_type": "LIMIT", 172 | "reject_reason": "REJECT_REASON_UNSPECIFIED", 173 | "settled": false, 174 | "product_type": "SPOT", 175 | "reject_message": "", 176 | "cancel_message": "User requested cancel" 177 | }, 178 | { 179 | "order_id": "dc98ccb7-8494-418d-99e9-651b76ca64f2", 180 | "product_id": "ALGO-USD", 181 | "user_id": "cd5d3286-b5ad-5f48-b356-1fd3e4e73f72", 182 | "order_configuration": { 183 | "limit_limit_gtc": { 184 | "base_size": "5", 185 | "limit_price": "0.9", 186 | "post_only": false 187 | } 188 | }, 189 | "side": "SELL", 190 | "client_order_id": "k7234k-mop0987klas-9098--asasa", 191 | "status": "CANCELLED", 192 | "time_in_force": "GOOD_UNTIL_CANCELLED", 193 | "created_time": "2023-01-29T06:26:14.759800Z", 194 | "completion_percentage": "0", 195 | "filled_size": "0", 196 | "average_filled_price": "0", 197 | "fee": "", 198 | "number_of_fills": "0", 199 | "filled_value": "0", 200 | "pending_cancel": false, 201 | "size_in_quote": false, 202 | "total_fees": "0", 203 | "size_inclusive_of_fees": false, 204 | "total_value_after_fees": "0", 205 | "trigger_status": "INVALID_ORDER_TYPE", 206 | "order_type": "LIMIT", 207 | "reject_reason": "REJECT_REASON_UNSPECIFIED", 208 | "settled": false, 209 | "product_type": "SPOT", 210 | "reject_message": "", 211 | "cancel_message": "User requested cancel" 212 | }, 213 | { 214 | "order_id": "7abd16cf-0117-4d00-9a66-af158365c2db", 215 | "product_id": "ALGO-USD", 216 | "user_id": "cd5d3286-b5ad-5f48-b356-1fd3e4e73f72", 217 | "order_configuration": { 218 | "limit_limit_gtc": { 219 | "base_size": "5", 220 | "limit_price": "0.9", 221 | "post_only": false 222 | } 223 | }, 224 | "side": "SELL", 225 | "client_order_id": "k7234k-mklas-9098--asasa", 226 | "status": "CANCELLED", 227 | "time_in_force": "GOOD_UNTIL_CANCELLED", 228 | "created_time": "2023-01-29T06:10:03.260178Z", 229 | "completion_percentage": "0", 230 | "filled_size": "0", 231 | "average_filled_price": "0", 232 | "fee": "", 233 | "number_of_fills": "0", 234 | "filled_value": "0", 235 | "pending_cancel": false, 236 | "size_in_quote": false, 237 | "total_fees": "0", 238 | "size_inclusive_of_fees": false, 239 | "total_value_after_fees": "0", 240 | "trigger_status": "INVALID_ORDER_TYPE", 241 | "order_type": "LIMIT", 242 | "reject_reason": "REJECT_REASON_UNSPECIFIED", 243 | "settled": false, 244 | "product_type": "SPOT", 245 | "reject_message": "", 246 | "cancel_message": "User requested cancel" 247 | }, 248 | { 249 | "order_id": "700128ed-a968-4345-b646-ebd8c167d0ec", 250 | "product_id": "ALGO-USD", 251 | "user_id": "cd5d3286-b5ad-5f48-b356-1fd3e4e73f72", 252 | "order_configuration": { 253 | "limit_limit_gtc": { 254 | "base_size": "5", 255 | "limit_price": "0.9", 256 | "post_only": false 257 | } 258 | }, 259 | "side": "SELL", 260 | "client_order_id": "k7234k-mklas--asasa", 261 | "status": "CANCELLED", 262 | "time_in_force": "GOOD_UNTIL_CANCELLED", 263 | "created_time": "2023-01-29T06:08:32.452578Z", 264 | "completion_percentage": "0", 265 | "filled_size": "0", 266 | "average_filled_price": "0", 267 | "fee": "", 268 | "number_of_fills": "0", 269 | "filled_value": "0", 270 | "pending_cancel": false, 271 | "size_in_quote": false, 272 | "total_fees": "0", 273 | "size_inclusive_of_fees": false, 274 | "total_value_after_fees": "0", 275 | "trigger_status": "INVALID_ORDER_TYPE", 276 | "order_type": "LIMIT", 277 | "reject_reason": "REJECT_REASON_UNSPECIFIED", 278 | "settled": false, 279 | "product_type": "SPOT", 280 | "reject_message": "", 281 | "cancel_message": "User requested cancel" 282 | }, 283 | { 284 | "order_id": "1f4e4db8-b9b1-4fed-a597-de404039a07b", 285 | "product_id": "ALGO-USD", 286 | "user_id": "cd5d3286-b5ad-5f48-b356-1fd3e4e73f72", 287 | "order_configuration": { 288 | "limit_limit_gtc": { 289 | "base_size": "5", 290 | "limit_price": "0.9", 291 | "post_only": false 292 | } 293 | }, 294 | "side": "SELL", 295 | "client_order_id": "k7234k-mklas-asa", 296 | "status": "CANCELLED", 297 | "time_in_force": "GOOD_UNTIL_CANCELLED", 298 | "created_time": "2023-01-29T06:05:39.923240Z", 299 | "completion_percentage": "0", 300 | "filled_size": "0", 301 | "average_filled_price": "0", 302 | "fee": "", 303 | "number_of_fills": "0", 304 | "filled_value": "0", 305 | "pending_cancel": false, 306 | "size_in_quote": false, 307 | "total_fees": "0", 308 | "size_inclusive_of_fees": false, 309 | "total_value_after_fees": "0", 310 | "trigger_status": "INVALID_ORDER_TYPE", 311 | "order_type": "LIMIT", 312 | "reject_reason": "REJECT_REASON_UNSPECIFIED", 313 | "settled": false, 314 | "product_type": "SPOT", 315 | "reject_message": "", 316 | "cancel_message": "User requested cancel" 317 | }, 318 | { 319 | "order_id": "e389f2c9-208c-435c-a1c8-b430948e531b", 320 | "product_id": "ALGO-USD", 321 | "user_id": "cd5d3286-b5ad-5f48-b356-1fd3e4e73f72", 322 | "order_configuration": { 323 | "limit_limit_gtc": { 324 | "base_size": "5", 325 | "limit_price": "0.9", 326 | "post_only": false 327 | } 328 | }, 329 | "side": "SELL", 330 | "client_order_id": "k7234k-jkashd879a", 331 | "status": "CANCELLED", 332 | "time_in_force": "GOOD_UNTIL_CANCELLED", 333 | "created_time": "2023-01-29T06:04:21.946256Z", 334 | "completion_percentage": "0", 335 | "filled_size": "0", 336 | "average_filled_price": "0", 337 | "fee": "", 338 | "number_of_fills": "0", 339 | "filled_value": "0", 340 | "pending_cancel": false, 341 | "size_in_quote": false, 342 | "total_fees": "0", 343 | "size_inclusive_of_fees": false, 344 | "total_value_after_fees": "0", 345 | "trigger_status": "INVALID_ORDER_TYPE", 346 | "order_type": "LIMIT", 347 | "reject_reason": "REJECT_REASON_UNSPECIFIED", 348 | "settled": false, 349 | "product_type": "SPOT", 350 | "reject_message": "", 351 | "cancel_message": "User requested cancel" 352 | } 353 | ], 354 | "sequence": "0", 355 | "has_next": true, 356 | "cursor": "somecursor" 357 | } -------------------------------------------------------------------------------- /coinbaseadvanced/models/market_data.py: -------------------------------------------------------------------------------- 1 | class Level2Event: 2 | def __init__(self, channel: str, client_id: str, timestamp: str, sequence_num: int, events: list): 3 | """ 4 | Initializes a Level2Event object. 5 | 6 | :param channel: The channel name. 7 | :param client_id: The client ID. 8 | :param timestamp: The timestamp of the event. 9 | :param sequence_num: The sequence number of the event. 10 | :param events: A list of event data. 11 | """ 12 | self.channel = channel 13 | self.client_id = client_id 14 | self.timestamp = timestamp 15 | self.sequence_num = sequence_num 16 | self.events = [L2Event(event) for event in events] 17 | 18 | def __repr__(self): 19 | return f"Level2Event(channel={self.channel}, events={self.events})" 20 | 21 | 22 | class L2Event: 23 | def __init__(self, event: dict): 24 | """ 25 | Initializes an L2Event object. 26 | 27 | :param event: A dictionary containing event data. 28 | """ 29 | self.type = event.get('type') 30 | self.product_id = event.get('product_id') 31 | self.updates = [L2Update(update) for update in event.get('updates', [])] 32 | 33 | def __repr__(self): 34 | return f"L2Event(type={self.type}, product_id={self.product_id}, updates={self.updates})" 35 | 36 | 37 | class L2Update: 38 | def __init__(self, update: dict): 39 | """ 40 | Initializes an L2Update object. 41 | 42 | :param update: A dictionary containing update data. 43 | """ 44 | self.side = update.get('side') 45 | self.event_time = update.get('event_time') 46 | self.price_level = update.get('price_level') 47 | self.new_quantity = update.get('new_quantity') 48 | 49 | def __repr__(self): 50 | return (f"L2Update(side={self.side}, event_time={self.event_time}, price_level={self.price_level}, " 51 | f"new_quantity={self.new_quantity})") 52 | 53 | 54 | class HeartbeatEvent: 55 | def __init__(self, channel: str, current_time: str, heartbeat_counter: int): 56 | """ 57 | Initializes a HeartbeatEvent object. 58 | 59 | :param channel: The channel name. 60 | :param current_time: The current time of the heartbeat event. 61 | :param heartbeat_counter: The heartbeat counter. 62 | """ 63 | self.channel = channel 64 | self.current_time = current_time 65 | self.heartbeat_counter = heartbeat_counter 66 | 67 | def __repr__(self): 68 | return (f"HeartbeatEvent(channel={self.channel}, current_time={self.current_time}, " 69 | f"heartbeat_counter={self.heartbeat_counter})") 70 | 71 | 72 | class CandlesEvent: 73 | def __init__(self, channel: str, client_id: str, timestamp: str, sequence_num: int, events: list): 74 | """ 75 | Initializes a CandlesEvent object. 76 | 77 | :param channel: The channel name. 78 | :param client_id: The client ID. 79 | :param timestamp: The timestamp of the event. 80 | :param sequence_num: The sequence number of the event. 81 | :param events: A list of event data. 82 | """ 83 | self.channel = channel 84 | self.client_id = client_id 85 | self.timestamp = timestamp 86 | self.sequence_num = sequence_num 87 | self.candles = [] 88 | for event in events: 89 | if 'candles' in event: 90 | self.candles.extend([CandleUpdate(update) for update in event['candles']]) 91 | 92 | def __repr__(self): 93 | return f"CandlesEvent(channel={self.channel}, candles={self.candles})" 94 | 95 | 96 | class CandleUpdate: 97 | def __init__(self, update: dict): 98 | """ 99 | Initializes a CandleUpdate object. 100 | 101 | :param update: A dictionary containing update data. 102 | """ 103 | self.start = update['start'] 104 | self.high = update['high'] 105 | self.low = update['low'] 106 | self.open = update['open'] 107 | self.close = update['close'] 108 | self.volume = update['volume'] 109 | self.product_id = update['product_id'] 110 | 111 | def __repr__(self): 112 | return (f"CandleUpdate(start={self.start}, high={self.high}, low={self.low}, open={self.open}, " 113 | f"close={self.close}, volume={self.volume}, product_id={self.product_id})") 114 | 115 | 116 | class MarketTradesEvent: 117 | def __init__(self, channel: str, client_id: str, timestamp: str, sequence_num: int, trades: list): 118 | """ 119 | Initializes a MarketTradesEvent object. 120 | 121 | :param channel: The channel name. 122 | :param client_id: The client ID. 123 | :param timestamp: The timestamp of the event. 124 | :param sequence_num: The sequence number of the event. 125 | :param trades: A list of trade data. 126 | """ 127 | self.channel = channel 128 | self.client_id = client_id 129 | self.timestamp = timestamp 130 | self.sequence_num = sequence_num 131 | self.trades = [TradeDetail(trade) for trade in trades] 132 | 133 | def __repr__(self): 134 | return f"MarketTradesEvent(channel={self.channel}, trades={self.trades})" 135 | 136 | 137 | class TradeDetail: 138 | def __init__(self, trade: dict): 139 | """ 140 | Initializes a TradeDetail object. 141 | 142 | :param trade: A dictionary containing trade data. 143 | """ 144 | self.trade_id = trade.get('trade_id') 145 | self.product_id = trade.get('product_id') 146 | self.price = trade.get('price') 147 | self.size = trade.get('size') 148 | self.side = trade.get('side') 149 | self.time = trade.get('time') 150 | 151 | def __repr__(self): 152 | return f"TradeDetail(product_id={self.product_id}, trade_id={self.trade_id}, price={self.price}, size={self.size}, side={self.side}, time={self.time})" 153 | 154 | 155 | class StatusEvent: 156 | def __init__(self, channel: str, client_id: str, timestamp: str, sequence_num: int, events: list): 157 | """ 158 | Initializes a StatusEvent object. 159 | 160 | :param channel: The channel name. 161 | :param client_id: The client ID. 162 | :param timestamp: The timestamp of the event. 163 | :param sequence_num: The sequence number of the event. 164 | :param events: A list of event data. 165 | """ 166 | self.channel = channel 167 | self.client_id = client_id 168 | self.timestamp = timestamp 169 | self.sequence_num = sequence_num 170 | self.products = [] 171 | for event in events: 172 | if 'products' in event: 173 | self.products.extend([ProductStatus(product) for product in event['products']]) 174 | 175 | def __repr__(self): 176 | return f"StatusEvent(channel={self.channel}, products={self.products})" 177 | 178 | 179 | class ProductStatus: 180 | def __init__(self, product: dict): 181 | """ 182 | Initializes a ProductStatus object. 183 | 184 | :param product: A dictionary containing product data. 185 | """ 186 | self.product_id = product.get('id') 187 | self.status = product.get('status') 188 | self.product_type = product.get('product_type') 189 | self.base_currency = product.get('base_currency') 190 | self.quote_currency = product.get('quote_currency') 191 | self.base_increment = product.get('base_increment') 192 | self.quote_increment = product.get('quote_increment') 193 | self.display_name = product.get('display_name') 194 | self.status_message = product.get('status_message') 195 | self.min_market_funds = product.get('min_market_funds') 196 | 197 | def __repr__(self): 198 | return (f"ProductStatus(product_id={self.product_id}, status={self.status}, product_type={self.product_type}, " 199 | f"base_currency={self.base_currency}, quote_currency={self.quote_currency}, base_increment={self.base_increment}, " 200 | f"quote_increment={self.quote_increment}, display_name={self.display_name}, status_message={self.status_message}, " 201 | f"min_market_funds={self.min_market_funds})") 202 | 203 | 204 | class TickerEvent: 205 | def __init__(self, channel: str, client_id: str, timestamp: str, sequence_num: int, events: list): 206 | """ 207 | Initializes a TickerEvent object. 208 | 209 | :param channel: The channel name. 210 | :param client_id: The client ID. 211 | :param timestamp: The timestamp of the event. 212 | :param sequence_num: The sequence number of the event. 213 | :param events: A list of event data. 214 | """ 215 | self.channel = channel 216 | self.client_id = client_id 217 | self.timestamp = timestamp 218 | self.sequence_num = sequence_num 219 | self.tickers = [] 220 | for event in events: 221 | if 'tickers' in event: 222 | self.tickers.extend([TickerDetail(ticker) for ticker in event['tickers']]) 223 | 224 | def __repr__(self): 225 | return f"TickerEvent(channel={self.channel}, tickers={self.tickers})" 226 | 227 | 228 | class TickerDetail: 229 | def __init__(self, ticker: dict): 230 | """ 231 | Initializes a TickerDetail object. 232 | 233 | :param ticker: A dictionary containing ticker data. 234 | """ 235 | self.type = ticker.get('type') 236 | self.product_id = ticker.get('product_id') 237 | self.price = ticker.get('price') 238 | self.volume_24_h = ticker.get('volume_24_h') 239 | self.low_24_h = ticker.get('low_24_h') 240 | self.high_24_h = ticker.get('high_24_h') 241 | self.low_52_w = ticker.get('low_52_w') 242 | self.high_52_w = ticker.get('high_52_w') 243 | self.price_percent_chg_24_h = ticker.get('price_percent_chg_24_h') 244 | self.best_bid = ticker.get('best_bid') 245 | self.best_ask = ticker.get('best_ask') 246 | self.best_bid_quantity = ticker.get('best_bid_quantity') 247 | self.best_ask_quantity = ticker.get('best_ask_quantity') 248 | 249 | def __repr__(self): 250 | return (f"TickerDetail(type={self.type}, product_id={self.product_id}, price={self.price}, " 251 | f"volume_24_h={self.volume_24_h}, low_24_h={self.low_24_h}, high_24_h={self.high_24_h}, " 252 | f"low_52_w={self.low_52_w}, high_52_w={self.high_52_w}, price_percent_chg_24_h={self.price_percent_chg_24_h}, " 253 | f"best_bid={self.best_bid}, best_ask={self.best_ask}, best_bid_quantity={self.best_bid_quantity}, " 254 | f"best_ask_quantity={self.best_ask_quantity})") 255 | 256 | 257 | class TickerBatchEvent: 258 | def __init__(self, channel: str, client_id: str, timestamp: str, sequence_num: int, events: list): 259 | """ 260 | Initializes a TickerBatchEvent object. 261 | 262 | :param channel: The channel name. 263 | :param client_id: The client ID. 264 | :param timestamp: The timestamp of the event. 265 | :param sequence_num: The sequence number of the event. 266 | :param events: A list of event data. 267 | """ 268 | self.channel = channel 269 | self.client_id = client_id 270 | self.timestamp = timestamp 271 | self.sequence_num = sequence_num 272 | self.tickers = [] 273 | for event in events: 274 | if 'tickers' in event: 275 | self.tickers.extend([TickerDetail(ticker) for ticker in event['tickers']]) 276 | 277 | def __repr__(self): 278 | return f"TickerBatchEvent(channel={self.channel}, tickers={self.tickers})" 279 | 280 | 281 | class UserEvent: 282 | def __init__( 283 | self, 284 | channel: str, 285 | client_id: str, 286 | timestamp: str, 287 | sequence_num: int, 288 | events: list, 289 | user_id: str = "", 290 | profile_id: str = ""): 291 | """ 292 | Initializes a UserEvent object. 293 | 294 | :param channel: The channel name. 295 | :param client_id: The client ID. 296 | :param timestamp: The timestamp of the event. 297 | :param sequence_num: The sequence number of the event. 298 | :param events: A list of event data. 299 | :param user_id: The user ID. 300 | :param profile_id: The profile ID. 301 | """ 302 | self.channel = channel 303 | self.client_id = client_id 304 | self.timestamp = timestamp 305 | self.sequence_num = sequence_num 306 | self.user_id = user_id 307 | self.profile_id = profile_id 308 | self.orders = [] 309 | self.positions = {"perpetual_futures_positions": [], "expiring_futures_positions": []} 310 | for event in events: 311 | if 'orders' in event: 312 | self.orders.extend([Order(order) for order in event['orders']]) 313 | if 'positions' in event: 314 | self.positions = event['positions'] 315 | 316 | def __repr__(self): 317 | return f"UserEvent(channel={self.channel}, user_id={self.user_id}, profile_id={self.profile_id}, orders={self.orders}, positions={self.positions})" 318 | 319 | 320 | class Order: 321 | def __init__(self, order: dict): 322 | """ 323 | Initializes an Order object. 324 | 325 | :param order: A dictionary containing order data. 326 | """ 327 | self.avg_price = order.get('avg_price') 328 | self.cancel_reason = order.get('cancel_reason') 329 | self.client_order_id = order.get('client_order_id') 330 | self.completion_percentage = order.get('completion_percentage') 331 | self.contract_expiry_type = order.get('contract_expiry_type') 332 | self.cumulative_quantity = order.get('cumulative_quantity') 333 | self.filled_value = order.get('filled_value') 334 | self.leaves_quantity = order.get('leaves_quantity') 335 | self.limit_price = order.get('limit_price') 336 | self.number_of_fills = order.get('number_of_fills') 337 | self.order_id = order.get('order_id') 338 | self.order_side = order.get('order_side') 339 | self.order_type = order.get('order_type') 340 | self.outstanding_hold_amount = order.get('outstanding_hold_amount') 341 | self.post_only = order.get('post_only') 342 | self.product_id = order.get('product_id') 343 | self.product_type = order.get('product_type') 344 | self.reject_reason = order.get('reject_reason') 345 | self.retail_portfolio_id = order.get('retail_portfolio_id') 346 | self.risk_managed_by = order.get('risk_managed_by') 347 | self.status = order.get('status') 348 | self.stop_price = order.get('stop_price') 349 | self.time_in_force = order.get('time_in_force') 350 | self.total_fees = order.get('total_fees') 351 | self.total_value_after_fees = order.get('total_value_after_fees') 352 | self.trigger_status = order.get('trigger_status') 353 | self.creation_time = order.get('creation_time') 354 | self.end_time = order.get('end_time') 355 | self.start_time = order.get('start_time') 356 | 357 | def __repr__(self): 358 | return (f"Order(order_id={self.order_id}, order_side={self.order_side}, order_type={self.order_type}, " 359 | f"status={self.status}, product_id={self.product_id}, limit_price={self.limit_price}, " 360 | f"leaves_quantity={self.leaves_quantity}, creation_time={self.creation_time})") 361 | -------------------------------------------------------------------------------- /tests/fixtures/list_orders_success_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "orders": [ 3 | { 4 | "order_id": "0a89e021-92f9-49a8-8657-d1a948c72e01", 5 | "product_id": "ALGO-USD", 6 | "user_id": "cd5d3286-b5ad-5f48-b356-1fd3e4e73f72", 7 | "order_configuration": { 8 | "limit_limit_gtc": { 9 | "base_size": "5", 10 | "limit_price": "0.9", 11 | "post_only": false 12 | } 13 | }, 14 | "side": "SELL", 15 | "client_order_id": "j876789ks", 16 | "status": "CANCELLED", 17 | "time_in_force": "GOOD_UNTIL_CANCELLED", 18 | "created_time": "2023-01-29T06:36:23.487310Z", 19 | "completion_percentage": "0", 20 | "filled_size": "0", 21 | "average_filled_price": "0", 22 | "fee": "", 23 | "number_of_fills": "0", 24 | "filled_value": "0", 25 | "pending_cancel": false, 26 | "size_in_quote": false, 27 | "total_fees": "0", 28 | "size_inclusive_of_fees": false, 29 | "total_value_after_fees": "0", 30 | "trigger_status": "INVALID_ORDER_TYPE", 31 | "order_type": "LIMIT", 32 | "reject_reason": "REJECT_REASON_UNSPECIFIED", 33 | "settled": false, 34 | "product_type": "SPOT", 35 | "reject_message": "", 36 | "cancel_message": "User requested cancel", 37 | "order_placement_source": "UNKNOWN_PLACEMENT_SOURCE", 38 | "outstanding_hold_amount": "0" 39 | }, 40 | { 41 | "order_id": "5f96405c-2246-445f-8eb8-839a63c1fe71", 42 | "product_id": "ALGO-USD", 43 | "user_id": "cd5d3286-b5ad-5f48-b356-1fd3e4e73f72", 44 | "order_configuration": { 45 | "limit_limit_gtc": { 46 | "base_size": "5", 47 | "limit_price": "0.9", 48 | "post_only": false 49 | } 50 | }, 51 | "side": "SELL", 52 | "client_order_id": "k7999902", 53 | "status": "CANCELLED", 54 | "time_in_force": "GOOD_UNTIL_CANCELLED", 55 | "created_time": "2023-01-29T06:36:23.275412Z", 56 | "completion_percentage": "0", 57 | "filled_size": "0", 58 | "average_filled_price": "0", 59 | "fee": "", 60 | "number_of_fills": "0", 61 | "filled_value": "0", 62 | "pending_cancel": false, 63 | "size_in_quote": false, 64 | "total_fees": "0", 65 | "size_inclusive_of_fees": false, 66 | "total_value_after_fees": "0", 67 | "trigger_status": "INVALID_ORDER_TYPE", 68 | "order_type": "LIMIT", 69 | "reject_reason": "REJECT_REASON_UNSPECIFIED", 70 | "settled": false, 71 | "product_type": "SPOT", 72 | "reject_message": "", 73 | "cancel_message": "User requested cancel", 74 | "order_placement_source": "UNKNOWN_PLACEMENT_SOURCE", 75 | "outstanding_hold_amount": "0" 76 | }, 77 | { 78 | "order_id": "4258a7d6-fbf9-4519-85c5-0a3707b8ee99", 79 | "product_id": "ALGO-USD", 80 | "user_id": "cd5d3286-b5ad-5f48-b356-1fd3e4e73f72", 81 | "order_configuration": { 82 | "limit_limit_gtc": { 83 | "base_size": "5", 84 | "limit_price": "0.9", 85 | "post_only": false 86 | } 87 | }, 88 | "side": "SELL", 89 | "client_order_id": "jks", 90 | "status": "CANCELLED", 91 | "time_in_force": "GOOD_UNTIL_CANCELLED", 92 | "created_time": "2023-01-29T06:34:09.957562Z", 93 | "completion_percentage": "0", 94 | "filled_size": "0", 95 | "average_filled_price": "0", 96 | "fee": "", 97 | "number_of_fills": "0", 98 | "filled_value": "0", 99 | "pending_cancel": false, 100 | "size_in_quote": false, 101 | "total_fees": "0", 102 | "size_inclusive_of_fees": false, 103 | "total_value_after_fees": "0", 104 | "trigger_status": "INVALID_ORDER_TYPE", 105 | "order_type": "LIMIT", 106 | "reject_reason": "REJECT_REASON_UNSPECIFIED", 107 | "settled": false, 108 | "product_type": "SPOT", 109 | "reject_message": "", 110 | "cancel_message": "User requested cancel", 111 | "order_placement_source": "UNKNOWN_PLACEMENT_SOURCE", 112 | "outstanding_hold_amount": "0" 113 | }, 114 | { 115 | "order_id": "dd87277f-ef6f-40c1-8c27-27238a7c1370", 116 | "product_id": "ALGO-USD", 117 | "user_id": "cd5d3286-b5ad-5f48-b356-1fd3e4e73f72", 118 | "order_configuration": { 119 | "limit_limit_gtc": { 120 | "base_size": "5", 121 | "limit_price": "0.9", 122 | "post_only": false 123 | } 124 | }, 125 | "side": "SELL", 126 | "client_order_id": "k72", 127 | "status": "CANCELLED", 128 | "time_in_force": "GOOD_UNTIL_CANCELLED", 129 | "created_time": "2023-01-29T06:34:09.682464Z", 130 | "completion_percentage": "0", 131 | "filled_size": "0", 132 | "average_filled_price": "0", 133 | "fee": "", 134 | "number_of_fills": "0", 135 | "filled_value": "0", 136 | "pending_cancel": false, 137 | "size_in_quote": false, 138 | "total_fees": "0", 139 | "size_inclusive_of_fees": false, 140 | "total_value_after_fees": "0", 141 | "trigger_status": "INVALID_ORDER_TYPE", 142 | "order_type": "LIMIT", 143 | "reject_reason": "REJECT_REASON_UNSPECIFIED", 144 | "settled": false, 145 | "product_type": "SPOT", 146 | "reject_message": "", 147 | "cancel_message": "User requested cancel", 148 | "order_placement_source": "UNKNOWN_PLACEMENT_SOURCE", 149 | "outstanding_hold_amount": "0" 150 | }, 151 | { 152 | "order_id": "2ab5a57e-f3e4-41fa-bf82-65e7d89640c0", 153 | "product_id": "ALGO-USD", 154 | "user_id": "cd5d3286-b5ad-5f48-b356-1fd3e4e73f72", 155 | "order_configuration": { 156 | "limit_limit_gtc": { 157 | "base_size": "5", 158 | "limit_price": "0.9", 159 | "post_only": false 160 | } 161 | }, 162 | "side": "SELL", 163 | "client_order_id": "jksd89n90s8df-900s8df", 164 | "status": "CANCELLED", 165 | "time_in_force": "GOOD_UNTIL_CANCELLED", 166 | "created_time": "2023-01-29T06:30:21.564102Z", 167 | "completion_percentage": "0", 168 | "filled_size": "0", 169 | "average_filled_price": "0", 170 | "fee": "", 171 | "number_of_fills": "0", 172 | "filled_value": "0", 173 | "pending_cancel": false, 174 | "size_in_quote": false, 175 | "total_fees": "0", 176 | "size_inclusive_of_fees": false, 177 | "total_value_after_fees": "0", 178 | "trigger_status": "INVALID_ORDER_TYPE", 179 | "order_type": "LIMIT", 180 | "reject_reason": "REJECT_REASON_UNSPECIFIED", 181 | "settled": false, 182 | "product_type": "SPOT", 183 | "reject_message": "", 184 | "cancel_message": "User requested cancel", 185 | "order_placement_source": "UNKNOWN_PLACEMENT_SOURCE", 186 | "outstanding_hold_amount": "0" 187 | }, 188 | { 189 | "order_id": "dc98ccb7-8494-418d-99e9-651b76ca64f2", 190 | "product_id": "ALGO-USD", 191 | "user_id": "cd5d3286-b5ad-5f48-b356-1fd3e4e73f72", 192 | "order_configuration": { 193 | "limit_limit_gtc": { 194 | "base_size": "5", 195 | "limit_price": "0.9", 196 | "post_only": false 197 | } 198 | }, 199 | "side": "SELL", 200 | "client_order_id": "k7234k-mop0987klas-9098--asasa", 201 | "status": "CANCELLED", 202 | "time_in_force": "GOOD_UNTIL_CANCELLED", 203 | "created_time": "2023-01-29T06:26:14.759800Z", 204 | "completion_percentage": "0", 205 | "filled_size": "0", 206 | "average_filled_price": "0", 207 | "fee": "", 208 | "number_of_fills": "0", 209 | "filled_value": "0", 210 | "pending_cancel": false, 211 | "size_in_quote": false, 212 | "total_fees": "0", 213 | "size_inclusive_of_fees": false, 214 | "total_value_after_fees": "0", 215 | "trigger_status": "INVALID_ORDER_TYPE", 216 | "order_type": "LIMIT", 217 | "reject_reason": "REJECT_REASON_UNSPECIFIED", 218 | "settled": false, 219 | "product_type": "SPOT", 220 | "reject_message": "", 221 | "cancel_message": "User requested cancel", 222 | "order_placement_source": "UNKNOWN_PLACEMENT_SOURCE", 223 | "outstanding_hold_amount": "0" 224 | }, 225 | { 226 | "order_id": "7abd16cf-0117-4d00-9a66-af158365c2db", 227 | "product_id": "ALGO-USD", 228 | "user_id": "cd5d3286-b5ad-5f48-b356-1fd3e4e73f72", 229 | "order_configuration": { 230 | "limit_limit_gtc": { 231 | "base_size": "5", 232 | "limit_price": "0.9", 233 | "post_only": false 234 | } 235 | }, 236 | "side": "SELL", 237 | "client_order_id": "k7234k-mklas-9098--asasa", 238 | "status": "CANCELLED", 239 | "time_in_force": "GOOD_UNTIL_CANCELLED", 240 | "created_time": "2023-01-29T06:10:03.260178Z", 241 | "completion_percentage": "0", 242 | "filled_size": "0", 243 | "average_filled_price": "0", 244 | "fee": "", 245 | "number_of_fills": "0", 246 | "filled_value": "0", 247 | "pending_cancel": false, 248 | "size_in_quote": false, 249 | "total_fees": "0", 250 | "size_inclusive_of_fees": false, 251 | "total_value_after_fees": "0", 252 | "trigger_status": "INVALID_ORDER_TYPE", 253 | "order_type": "LIMIT", 254 | "reject_reason": "REJECT_REASON_UNSPECIFIED", 255 | "settled": false, 256 | "product_type": "SPOT", 257 | "reject_message": "", 258 | "cancel_message": "User requested cancel", 259 | "order_placement_source": "UNKNOWN_PLACEMENT_SOURCE", 260 | "outstanding_hold_amount": "0" 261 | }, 262 | { 263 | "order_id": "700128ed-a968-4345-b646-ebd8c167d0ec", 264 | "product_id": "ALGO-USD", 265 | "user_id": "cd5d3286-b5ad-5f48-b356-1fd3e4e73f72", 266 | "order_configuration": { 267 | "limit_limit_gtc": { 268 | "base_size": "5", 269 | "limit_price": "0.9", 270 | "post_only": false 271 | } 272 | }, 273 | "side": "SELL", 274 | "client_order_id": "k7234k-mklas--asasa", 275 | "status": "CANCELLED", 276 | "time_in_force": "GOOD_UNTIL_CANCELLED", 277 | "created_time": "2023-01-29T06:08:32.452578Z", 278 | "completion_percentage": "0", 279 | "filled_size": "0", 280 | "average_filled_price": "0", 281 | "fee": "", 282 | "number_of_fills": "0", 283 | "filled_value": "0", 284 | "pending_cancel": false, 285 | "size_in_quote": false, 286 | "total_fees": "0", 287 | "size_inclusive_of_fees": false, 288 | "total_value_after_fees": "0", 289 | "trigger_status": "INVALID_ORDER_TYPE", 290 | "order_type": "LIMIT", 291 | "reject_reason": "REJECT_REASON_UNSPECIFIED", 292 | "settled": false, 293 | "product_type": "SPOT", 294 | "reject_message": "", 295 | "cancel_message": "User requested cancel", 296 | "order_placement_source": "UNKNOWN_PLACEMENT_SOURCE", 297 | "outstanding_hold_amount": "0" 298 | }, 299 | { 300 | "order_id": "1f4e4db8-b9b1-4fed-a597-de404039a07b", 301 | "product_id": "ALGO-USD", 302 | "user_id": "cd5d3286-b5ad-5f48-b356-1fd3e4e73f72", 303 | "order_configuration": { 304 | "limit_limit_gtc": { 305 | "base_size": "5", 306 | "limit_price": "0.9", 307 | "post_only": false 308 | } 309 | }, 310 | "side": "SELL", 311 | "client_order_id": "k7234k-mklas-asa", 312 | "status": "CANCELLED", 313 | "time_in_force": "GOOD_UNTIL_CANCELLED", 314 | "created_time": "2023-01-29T06:05:39.923240Z", 315 | "completion_percentage": "0", 316 | "filled_size": "0", 317 | "average_filled_price": "0", 318 | "fee": "", 319 | "number_of_fills": "0", 320 | "filled_value": "0", 321 | "pending_cancel": false, 322 | "size_in_quote": false, 323 | "total_fees": "0", 324 | "size_inclusive_of_fees": false, 325 | "total_value_after_fees": "0", 326 | "trigger_status": "INVALID_ORDER_TYPE", 327 | "order_type": "LIMIT", 328 | "reject_reason": "REJECT_REASON_UNSPECIFIED", 329 | "settled": false, 330 | "product_type": "SPOT", 331 | "reject_message": "", 332 | "cancel_message": "User requested cancel", 333 | "order_placement_source": "UNKNOWN_PLACEMENT_SOURCE", 334 | "outstanding_hold_amount": "0" 335 | }, 336 | { 337 | "order_id": "e389f2c9-208c-435c-a1c8-b430948e531b", 338 | "product_id": "ALGO-USD", 339 | "user_id": "cd5d3286-b5ad-5f48-b356-1fd3e4e73f72", 340 | "order_configuration": { 341 | "limit_limit_gtc": { 342 | "base_size": "5", 343 | "limit_price": "0.9", 344 | "post_only": false 345 | } 346 | }, 347 | "side": "SELL", 348 | "client_order_id": "k7234k-jkashd879a", 349 | "status": "CANCELLED", 350 | "time_in_force": "GOOD_UNTIL_CANCELLED", 351 | "created_time": "2023-01-29T06:04:21.946256Z", 352 | "completion_percentage": "0", 353 | "filled_size": "0", 354 | "average_filled_price": "0", 355 | "fee": "", 356 | "number_of_fills": "0", 357 | "filled_value": "0", 358 | "pending_cancel": false, 359 | "size_in_quote": false, 360 | "total_fees": "0", 361 | "size_inclusive_of_fees": false, 362 | "total_value_after_fees": "0", 363 | "trigger_status": "INVALID_ORDER_TYPE", 364 | "order_type": "LIMIT", 365 | "reject_reason": "REJECT_REASON_UNSPECIFIED", 366 | "settled": false, 367 | "product_type": "SPOT", 368 | "reject_message": "", 369 | "cancel_message": "User requested cancel", 370 | "order_placement_source": "UNKNOWN_PLACEMENT_SOURCE", 371 | "outstanding_hold_amount": "0" 372 | } 373 | ], 374 | "sequence": "0", 375 | "has_next": true, 376 | "cursor": "148401249" 377 | } -------------------------------------------------------------------------------- /tests/fixtures/list_orders_with_extra_unnamed_success_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "orders": [ 3 | { 4 | "order_id": "0a89e021-92f9-49a8-8657-d1a948c72e01", 5 | "product_id": "ALGO-USD", 6 | "user_id": "cd5d3286-b5ad-5f48-b356-1fd3e4e73f72", 7 | "order_configuration": { 8 | "limit_limit_gtc": { 9 | "base_size": "5", 10 | "limit_price": "0.9", 11 | "post_only": false 12 | } 13 | }, 14 | "side": "SELL", 15 | "client_order_id": "j876789ks", 16 | "status": "CANCELLED", 17 | "time_in_force": "GOOD_UNTIL_CANCELLED", 18 | "created_time": "2023-01-29T06:36:23.487310Z", 19 | "completion_percentage": "0", 20 | "filled_size": "0", 21 | "average_filled_price": "0", 22 | "fee": "", 23 | "number_of_fills": "0", 24 | "filled_value": "0", 25 | "pending_cancel": false, 26 | "size_in_quote": false, 27 | "total_fees": "0", 28 | "size_inclusive_of_fees": false, 29 | "total_value_after_fees": "0", 30 | "trigger_status": "INVALID_ORDER_TYPE", 31 | "order_type": "LIMIT", 32 | "reject_reason": "REJECT_REASON_UNSPECIFIED", 33 | "settled": false, 34 | "product_type": "SPOT", 35 | "reject_message": "", 36 | "cancel_message": "User requested cancel", 37 | "order_placement_source": "UNKNOWN_PLACEMENT_SOURCE", 38 | "outstanding_hold_amount": "0", 39 | "extra_unnamed_arg": "0" 40 | }, 41 | { 42 | "order_id": "5f96405c-2246-445f-8eb8-839a63c1fe71", 43 | "product_id": "ALGO-USD", 44 | "user_id": "cd5d3286-b5ad-5f48-b356-1fd3e4e73f72", 45 | "order_configuration": { 46 | "limit_limit_gtc": { 47 | "base_size": "5", 48 | "limit_price": "0.9", 49 | "post_only": false 50 | } 51 | }, 52 | "side": "SELL", 53 | "client_order_id": "k7999902", 54 | "status": "CANCELLED", 55 | "time_in_force": "GOOD_UNTIL_CANCELLED", 56 | "created_time": "2023-01-29T06:36:23.275412Z", 57 | "completion_percentage": "0", 58 | "filled_size": "0", 59 | "average_filled_price": "0", 60 | "fee": "", 61 | "number_of_fills": "0", 62 | "filled_value": "0", 63 | "pending_cancel": false, 64 | "size_in_quote": false, 65 | "total_fees": "0", 66 | "size_inclusive_of_fees": false, 67 | "total_value_after_fees": "0", 68 | "trigger_status": "INVALID_ORDER_TYPE", 69 | "order_type": "LIMIT", 70 | "reject_reason": "REJECT_REASON_UNSPECIFIED", 71 | "settled": false, 72 | "product_type": "SPOT", 73 | "reject_message": "", 74 | "cancel_message": "User requested cancel", 75 | "order_placement_source": "UNKNOWN_PLACEMENT_SOURCE", 76 | "outstanding_hold_amount": "0" 77 | }, 78 | { 79 | "order_id": "4258a7d6-fbf9-4519-85c5-0a3707b8ee99", 80 | "product_id": "ALGO-USD", 81 | "user_id": "cd5d3286-b5ad-5f48-b356-1fd3e4e73f72", 82 | "order_configuration": { 83 | "limit_limit_gtc": { 84 | "base_size": "5", 85 | "limit_price": "0.9", 86 | "post_only": false 87 | } 88 | }, 89 | "side": "SELL", 90 | "client_order_id": "jks", 91 | "status": "CANCELLED", 92 | "time_in_force": "GOOD_UNTIL_CANCELLED", 93 | "created_time": "2023-01-29T06:34:09.957562Z", 94 | "completion_percentage": "0", 95 | "filled_size": "0", 96 | "average_filled_price": "0", 97 | "fee": "", 98 | "number_of_fills": "0", 99 | "filled_value": "0", 100 | "pending_cancel": false, 101 | "size_in_quote": false, 102 | "total_fees": "0", 103 | "size_inclusive_of_fees": false, 104 | "total_value_after_fees": "0", 105 | "trigger_status": "INVALID_ORDER_TYPE", 106 | "order_type": "LIMIT", 107 | "reject_reason": "REJECT_REASON_UNSPECIFIED", 108 | "settled": false, 109 | "product_type": "SPOT", 110 | "reject_message": "", 111 | "cancel_message": "User requested cancel", 112 | "order_placement_source": "UNKNOWN_PLACEMENT_SOURCE", 113 | "outstanding_hold_amount": "0" 114 | }, 115 | { 116 | "order_id": "dd87277f-ef6f-40c1-8c27-27238a7c1370", 117 | "product_id": "ALGO-USD", 118 | "user_id": "cd5d3286-b5ad-5f48-b356-1fd3e4e73f72", 119 | "order_configuration": { 120 | "limit_limit_gtc": { 121 | "base_size": "5", 122 | "limit_price": "0.9", 123 | "post_only": false 124 | } 125 | }, 126 | "side": "SELL", 127 | "client_order_id": "k72", 128 | "status": "CANCELLED", 129 | "time_in_force": "GOOD_UNTIL_CANCELLED", 130 | "created_time": "2023-01-29T06:34:09.682464Z", 131 | "completion_percentage": "0", 132 | "filled_size": "0", 133 | "average_filled_price": "0", 134 | "fee": "", 135 | "number_of_fills": "0", 136 | "filled_value": "0", 137 | "pending_cancel": false, 138 | "size_in_quote": false, 139 | "total_fees": "0", 140 | "size_inclusive_of_fees": false, 141 | "total_value_after_fees": "0", 142 | "trigger_status": "INVALID_ORDER_TYPE", 143 | "order_type": "LIMIT", 144 | "reject_reason": "REJECT_REASON_UNSPECIFIED", 145 | "settled": false, 146 | "product_type": "SPOT", 147 | "reject_message": "", 148 | "cancel_message": "User requested cancel", 149 | "order_placement_source": "UNKNOWN_PLACEMENT_SOURCE", 150 | "outstanding_hold_amount": "0" 151 | }, 152 | { 153 | "order_id": "2ab5a57e-f3e4-41fa-bf82-65e7d89640c0", 154 | "product_id": "ALGO-USD", 155 | "user_id": "cd5d3286-b5ad-5f48-b356-1fd3e4e73f72", 156 | "order_configuration": { 157 | "limit_limit_gtc": { 158 | "base_size": "5", 159 | "limit_price": "0.9", 160 | "post_only": false 161 | } 162 | }, 163 | "side": "SELL", 164 | "client_order_id": "jksd89n90s8df-900s8df", 165 | "status": "CANCELLED", 166 | "time_in_force": "GOOD_UNTIL_CANCELLED", 167 | "created_time": "2023-01-29T06:30:21.564102Z", 168 | "completion_percentage": "0", 169 | "filled_size": "0", 170 | "average_filled_price": "0", 171 | "fee": "", 172 | "number_of_fills": "0", 173 | "filled_value": "0", 174 | "pending_cancel": false, 175 | "size_in_quote": false, 176 | "total_fees": "0", 177 | "size_inclusive_of_fees": false, 178 | "total_value_after_fees": "0", 179 | "trigger_status": "INVALID_ORDER_TYPE", 180 | "order_type": "LIMIT", 181 | "reject_reason": "REJECT_REASON_UNSPECIFIED", 182 | "settled": false, 183 | "product_type": "SPOT", 184 | "reject_message": "", 185 | "cancel_message": "User requested cancel", 186 | "order_placement_source": "UNKNOWN_PLACEMENT_SOURCE", 187 | "outstanding_hold_amount": "0" 188 | }, 189 | { 190 | "order_id": "dc98ccb7-8494-418d-99e9-651b76ca64f2", 191 | "product_id": "ALGO-USD", 192 | "user_id": "cd5d3286-b5ad-5f48-b356-1fd3e4e73f72", 193 | "order_configuration": { 194 | "limit_limit_gtc": { 195 | "base_size": "5", 196 | "limit_price": "0.9", 197 | "post_only": false 198 | } 199 | }, 200 | "side": "SELL", 201 | "client_order_id": "k7234k-mop0987klas-9098--asasa", 202 | "status": "CANCELLED", 203 | "time_in_force": "GOOD_UNTIL_CANCELLED", 204 | "created_time": "2023-01-29T06:26:14.759800Z", 205 | "completion_percentage": "0", 206 | "filled_size": "0", 207 | "average_filled_price": "0", 208 | "fee": "", 209 | "number_of_fills": "0", 210 | "filled_value": "0", 211 | "pending_cancel": false, 212 | "size_in_quote": false, 213 | "total_fees": "0", 214 | "size_inclusive_of_fees": false, 215 | "total_value_after_fees": "0", 216 | "trigger_status": "INVALID_ORDER_TYPE", 217 | "order_type": "LIMIT", 218 | "reject_reason": "REJECT_REASON_UNSPECIFIED", 219 | "settled": false, 220 | "product_type": "SPOT", 221 | "reject_message": "", 222 | "cancel_message": "User requested cancel", 223 | "order_placement_source": "UNKNOWN_PLACEMENT_SOURCE", 224 | "outstanding_hold_amount": "0" 225 | }, 226 | { 227 | "order_id": "7abd16cf-0117-4d00-9a66-af158365c2db", 228 | "product_id": "ALGO-USD", 229 | "user_id": "cd5d3286-b5ad-5f48-b356-1fd3e4e73f72", 230 | "order_configuration": { 231 | "limit_limit_gtc": { 232 | "base_size": "5", 233 | "limit_price": "0.9", 234 | "post_only": false 235 | } 236 | }, 237 | "side": "SELL", 238 | "client_order_id": "k7234k-mklas-9098--asasa", 239 | "status": "CANCELLED", 240 | "time_in_force": "GOOD_UNTIL_CANCELLED", 241 | "created_time": "2023-01-29T06:10:03.260178Z", 242 | "completion_percentage": "0", 243 | "filled_size": "0", 244 | "average_filled_price": "0", 245 | "fee": "", 246 | "number_of_fills": "0", 247 | "filled_value": "0", 248 | "pending_cancel": false, 249 | "size_in_quote": false, 250 | "total_fees": "0", 251 | "size_inclusive_of_fees": false, 252 | "total_value_after_fees": "0", 253 | "trigger_status": "INVALID_ORDER_TYPE", 254 | "order_type": "LIMIT", 255 | "reject_reason": "REJECT_REASON_UNSPECIFIED", 256 | "settled": false, 257 | "product_type": "SPOT", 258 | "reject_message": "", 259 | "cancel_message": "User requested cancel", 260 | "order_placement_source": "UNKNOWN_PLACEMENT_SOURCE", 261 | "outstanding_hold_amount": "0" 262 | }, 263 | { 264 | "order_id": "700128ed-a968-4345-b646-ebd8c167d0ec", 265 | "product_id": "ALGO-USD", 266 | "user_id": "cd5d3286-b5ad-5f48-b356-1fd3e4e73f72", 267 | "order_configuration": { 268 | "limit_limit_gtc": { 269 | "base_size": "5", 270 | "limit_price": "0.9", 271 | "post_only": false 272 | } 273 | }, 274 | "side": "SELL", 275 | "client_order_id": "k7234k-mklas--asasa", 276 | "status": "CANCELLED", 277 | "time_in_force": "GOOD_UNTIL_CANCELLED", 278 | "created_time": "2023-01-29T06:08:32.452578Z", 279 | "completion_percentage": "0", 280 | "filled_size": "0", 281 | "average_filled_price": "0", 282 | "fee": "", 283 | "number_of_fills": "0", 284 | "filled_value": "0", 285 | "pending_cancel": false, 286 | "size_in_quote": false, 287 | "total_fees": "0", 288 | "size_inclusive_of_fees": false, 289 | "total_value_after_fees": "0", 290 | "trigger_status": "INVALID_ORDER_TYPE", 291 | "order_type": "LIMIT", 292 | "reject_reason": "REJECT_REASON_UNSPECIFIED", 293 | "settled": false, 294 | "product_type": "SPOT", 295 | "reject_message": "", 296 | "cancel_message": "User requested cancel", 297 | "order_placement_source": "UNKNOWN_PLACEMENT_SOURCE", 298 | "outstanding_hold_amount": "0" 299 | }, 300 | { 301 | "order_id": "1f4e4db8-b9b1-4fed-a597-de404039a07b", 302 | "product_id": "ALGO-USD", 303 | "user_id": "cd5d3286-b5ad-5f48-b356-1fd3e4e73f72", 304 | "order_configuration": { 305 | "limit_limit_gtc": { 306 | "base_size": "5", 307 | "limit_price": "0.9", 308 | "post_only": false 309 | } 310 | }, 311 | "side": "SELL", 312 | "client_order_id": "k7234k-mklas-asa", 313 | "status": "CANCELLED", 314 | "time_in_force": "GOOD_UNTIL_CANCELLED", 315 | "created_time": "2023-01-29T06:05:39.923240Z", 316 | "completion_percentage": "0", 317 | "filled_size": "0", 318 | "average_filled_price": "0", 319 | "fee": "", 320 | "number_of_fills": "0", 321 | "filled_value": "0", 322 | "pending_cancel": false, 323 | "size_in_quote": false, 324 | "total_fees": "0", 325 | "size_inclusive_of_fees": false, 326 | "total_value_after_fees": "0", 327 | "trigger_status": "INVALID_ORDER_TYPE", 328 | "order_type": "LIMIT", 329 | "reject_reason": "REJECT_REASON_UNSPECIFIED", 330 | "settled": false, 331 | "product_type": "SPOT", 332 | "reject_message": "", 333 | "cancel_message": "User requested cancel", 334 | "order_placement_source": "UNKNOWN_PLACEMENT_SOURCE", 335 | "outstanding_hold_amount": "0" 336 | }, 337 | { 338 | "order_id": "e389f2c9-208c-435c-a1c8-b430948e531b", 339 | "product_id": "ALGO-USD", 340 | "user_id": "cd5d3286-b5ad-5f48-b356-1fd3e4e73f72", 341 | "order_configuration": { 342 | "limit_limit_gtc": { 343 | "base_size": "5", 344 | "limit_price": "0.9", 345 | "post_only": false 346 | } 347 | }, 348 | "side": "SELL", 349 | "client_order_id": "k7234k-jkashd879a", 350 | "status": "CANCELLED", 351 | "time_in_force": "GOOD_UNTIL_CANCELLED", 352 | "created_time": "2023-01-29T06:04:21.946256Z", 353 | "completion_percentage": "0", 354 | "filled_size": "0", 355 | "average_filled_price": "0", 356 | "fee": "", 357 | "number_of_fills": "0", 358 | "filled_value": "0", 359 | "pending_cancel": false, 360 | "size_in_quote": false, 361 | "total_fees": "0", 362 | "size_inclusive_of_fees": false, 363 | "total_value_after_fees": "0", 364 | "trigger_status": "INVALID_ORDER_TYPE", 365 | "order_type": "LIMIT", 366 | "reject_reason": "REJECT_REASON_UNSPECIFIED", 367 | "settled": false, 368 | "product_type": "SPOT", 369 | "reject_message": "", 370 | "cancel_message": "User requested cancel", 371 | "order_placement_source": "UNKNOWN_PLACEMENT_SOURCE", 372 | "outstanding_hold_amount": "0" 373 | } 374 | ], 375 | "sequence": "0", 376 | "has_next": true, 377 | "cursor": "148401249" 378 | } -------------------------------------------------------------------------------- /coinbaseadvanced/models/orders.py: -------------------------------------------------------------------------------- 1 | """ 2 | Object models for order related endpoints args and response. 3 | """ 4 | 5 | from typing import List, Optional 6 | from datetime import datetime 7 | from enum import Enum 8 | 9 | import requests 10 | 11 | from coinbaseadvanced.models.common import BaseModel 12 | from coinbaseadvanced.models.error import CoinbaseAdvancedTradeAPIError 13 | 14 | 15 | class Side(Enum): 16 | """ 17 | Enum representing whether "BUY" or "SELL" order. 18 | """ 19 | 20 | BUY = "BUY" 21 | SELL = "SELL" 22 | 23 | 24 | class StopDirection(Enum): 25 | """ 26 | Enum direction in an stop order context. 27 | """ 28 | 29 | UNKNOWN = "UNKNOWN_STOP_DIRECTION" 30 | UP = "STOP_DIRECTION_STOP_UP" 31 | DOWN = "STOP_DIRECTION_STOP_DOWN" 32 | 33 | 34 | class OrderType(Enum): 35 | """ 36 | Enum representing different order types. 37 | """ 38 | 39 | UNKNOWN_ORDER_TYPE = "UNKNOWN_ORDER_TYPE" 40 | MARKET = "MARKET" 41 | LIMIT = "LIMIT" 42 | STOP = "STOP" 43 | STOP_LIMIT = "STOP_LIMIT" 44 | 45 | 46 | class OrderPlacementSource(Enum): 47 | """ 48 | Enum representing placements source for an order. 49 | """ 50 | UNKNOWN = "UNKNOWN_PLACEMENT_SOURCE" 51 | RETAIL_ADVANCDED = "RETAIL_ADVANCED" 52 | 53 | 54 | class OrderError(BaseModel): 55 | """ 56 | Class encapsulating order error fields. 57 | """ 58 | 59 | error: str 60 | message: str 61 | error_details: str 62 | preview_failure_reason: str 63 | new_order_failure_reason: str 64 | 65 | def __init__(self, 66 | error: str = '', 67 | message: str = '', 68 | error_details: str = '', 69 | preview_failure_reason: str = '', 70 | new_order_failure_reason: str = '', **kwargs) -> None: 71 | self.error = error 72 | self.message = message 73 | self.error_details = error_details 74 | self.preview_failure_reason = preview_failure_reason 75 | self.new_order_failure_reason = new_order_failure_reason 76 | 77 | self.kwargs = kwargs 78 | 79 | 80 | class LimitGTC(BaseModel): 81 | """ 82 | Limit till cancelled order configuration. 83 | """ 84 | 85 | base_size: str 86 | limit_price: str 87 | post_only: bool 88 | 89 | def __init__(self, base_size: str, limit_price: str, post_only: bool, **kwargs) -> None: 90 | self.base_size = base_size 91 | self.limit_price = limit_price 92 | self.post_only = post_only 93 | 94 | self.kwargs = kwargs 95 | 96 | 97 | class LimitGTD(BaseModel): 98 | """ 99 | Limit till date order configuration. 100 | """ 101 | 102 | base_size: str 103 | limit_price: str 104 | post_only: bool 105 | end_time: datetime 106 | 107 | def __init__(self, 108 | base_size: str, 109 | limit_price: str, 110 | post_only: bool, 111 | end_time: str, **kwargs) -> None: 112 | self.base_size = base_size 113 | self.limit_price = limit_price 114 | self.post_only = post_only 115 | self.end_time = datetime.strptime(end_time if len( 116 | end_time) <= 27 else end_time[:26]+'Z', "%Y-%m-%dT%H:%M:%SZ") 117 | 118 | self.kwargs = kwargs 119 | 120 | 121 | class MarketIOC(BaseModel): 122 | """ 123 | Market order configuration. 124 | """ 125 | 126 | quote_size: Optional[str] 127 | base_size: Optional[str] 128 | 129 | def __init__(self, 130 | quote_size: Optional[str] = None, 131 | base_size: Optional[str] = None, **kwargs) -> None: 132 | self.quote_size = quote_size 133 | self.base_size = base_size 134 | 135 | self.kwargs = kwargs 136 | 137 | 138 | class StopLimitGTC(BaseModel): 139 | """ 140 | Stop-Limit till cancelled order configuration. 141 | """ 142 | 143 | base_size: str 144 | limit_price: str 145 | stop_price: str 146 | stop_direction: str 147 | 148 | def __init__(self, 149 | base_size: str, 150 | limit_price: str, 151 | stop_price: str, 152 | stop_direction: str, **kwargs) -> None: 153 | self.base_size = base_size 154 | self.limit_price = limit_price 155 | self.stop_price = stop_price 156 | self.stop_direction = stop_direction 157 | 158 | self.kwargs = kwargs 159 | 160 | 161 | class StopLimitGTD(BaseModel): 162 | """ 163 | Stop-Limit till date order configuration. 164 | """ 165 | 166 | base_size: float 167 | limit_price: str 168 | stop_price: str 169 | end_time: datetime 170 | stop_direction: str 171 | 172 | def __init__(self, 173 | base_size: float, 174 | limit_price: str, 175 | stop_price: str, 176 | end_time: str, 177 | stop_direction: str, **kwargs) -> None: 178 | self.base_size = base_size 179 | self.limit_price = limit_price 180 | self.stop_price = stop_price 181 | self.end_time = datetime.strptime(end_time if len( 182 | end_time) <= 27 else end_time[:26]+'Z', "%Y-%m-%dT%H:%M:%SZ") 183 | self.stop_direction = stop_direction 184 | 185 | self.kwargs = kwargs 186 | 187 | 188 | class OrderEditRecord(BaseModel): 189 | """ 190 | Stop-Limit till date order configuration. 191 | """ 192 | 193 | def __init__(self, 194 | price: str, 195 | size: str, 196 | replace_accept_timestamp: str, 197 | **kwargs) -> None: 198 | self.price = price 199 | self.size = size 200 | self.replace_accept_timestamp = replace_accept_timestamp 201 | 202 | self.kwargs = kwargs 203 | 204 | 205 | class OrderConfiguration(BaseModel): 206 | """ 207 | Order Configuration. One of four possible fields should only be settled. 208 | """ 209 | 210 | market_market_ioc: Optional[MarketIOC] 211 | limit_limit_gtc: Optional[LimitGTC] 212 | limit_limit_gtd: Optional[LimitGTD] 213 | stop_limit_stop_limit_gtc: Optional[StopLimitGTC] 214 | stop_limit_stop_limit_gtd: Optional[StopLimitGTD] 215 | 216 | def __init__(self, 217 | market_market_ioc: Optional[dict] = None, 218 | limit_limit_gtc: Optional[dict] = None, 219 | limit_limit_gtd: Optional[dict] = None, 220 | stop_limit_stop_limit_gtc: Optional[dict] = None, 221 | stop_limit_stop_limit_gtd: Optional[dict] = None, **kwargs) -> None: 222 | self.market_market_ioc = MarketIOC( 223 | **market_market_ioc) if market_market_ioc is not None else None 224 | self.limit_limit_gtc = LimitGTC( 225 | **limit_limit_gtc) if limit_limit_gtc is not None else None 226 | self.limit_limit_gtd = LimitGTD( 227 | **limit_limit_gtd) if limit_limit_gtd is not None else None 228 | self.stop_limit_stop_limit_gtc = StopLimitGTC( 229 | **stop_limit_stop_limit_gtc) if stop_limit_stop_limit_gtc is not None else None 230 | self.stop_limit_stop_limit_gtd = StopLimitGTD( 231 | **stop_limit_stop_limit_gtd) if stop_limit_stop_limit_gtd is not None else None 232 | 233 | self.kwargs = kwargs 234 | 235 | 236 | class Order(BaseModel): 237 | """ 238 | Class reprensenting an order. This support the `create_order*` endpoints 239 | and the `get_order` endpoint. 240 | Fields will be filled depending on which endpoint generated the order since 241 | not all of them are returned at creation time. 242 | """ 243 | 244 | order_id: Optional[str] 245 | product_id: Optional[str] 246 | side: Optional[str] 247 | client_order_id: Optional[str] 248 | order_configuration: Optional[OrderConfiguration] 249 | 250 | user_id: Optional[str] 251 | status: Optional[str] 252 | time_in_force: Optional[str] 253 | created_time: Optional[datetime] 254 | completion_percentage: Optional[int] 255 | filled_size: Optional[str] 256 | average_filled_price: Optional[int] 257 | fee: Optional[str] 258 | number_of_fills: Optional[int] 259 | filled_value: Optional[int] 260 | pending_cancel: Optional[bool] 261 | size_in_quote: Optional[bool] 262 | total_fees: Optional[str] 263 | size_inclusive_of_fees: Optional[bool] 264 | total_value_after_fees: Optional[str] 265 | trigger_status: Optional[str] 266 | order_type: Optional[str] 267 | reject_reason: Optional[str] 268 | settled: Optional[str] 269 | product_type: Optional[str] 270 | reject_message: Optional[str] 271 | cancel_message: Optional[str] 272 | order_placement_source: Optional[str] 273 | outstanding_hold_amount: Optional[str] 274 | 275 | is_liquidation: Optional[bool] 276 | last_fill_time: Optional[str] 277 | edit_history: Optional[List[OrderEditRecord]] 278 | leverage: Optional[str] 279 | margin_type: Optional[str] 280 | 281 | order_error: Optional[OrderError] 282 | 283 | def __init__(self, 284 | order_id: Optional[str], 285 | product_id: Optional[str], 286 | side: Optional[str], 287 | client_order_id: Optional[str], 288 | order_configuration: Optional[dict], 289 | user_id: Optional[str] = None, 290 | status: Optional[str] = None, 291 | time_in_force: Optional[str] = None, 292 | created_time: Optional[str] = None, 293 | completion_percentage: Optional[int] = None, 294 | filled_size: Optional[str] = None, 295 | average_filled_price: Optional[int] = None, 296 | fee: Optional[str] = None, 297 | number_of_fills: Optional[int] = None, 298 | filled_value: Optional[int] = None, 299 | pending_cancel: Optional[bool] = None, 300 | size_in_quote: Optional[bool] = None, 301 | total_fees: Optional[str] = None, 302 | size_inclusive_of_fees: Optional[bool] = None, 303 | total_value_after_fees: Optional[str] = None, 304 | trigger_status: Optional[str] = None, 305 | order_type: Optional[str] = None, 306 | reject_reason: Optional[str] = None, 307 | settled: Optional[str] = None, 308 | product_type: Optional[str] = None, 309 | reject_message: Optional[str] = None, 310 | cancel_message: Optional[str] = None, 311 | order_placement_source: Optional[str] = None, 312 | outstanding_hold_amount: Optional[str] = None, 313 | 314 | is_liquidation: Optional[bool] = None, 315 | last_fill_time: Optional[str] = None, 316 | edit_history: Optional[List[OrderEditRecord]] = None, 317 | leverage: Optional[str] = None, 318 | margin_type: Optional[str] = None, 319 | 320 | order_error: Optional[dict] = None, **kwargs) -> None: 321 | self.order_id = order_id 322 | self.product_id = product_id 323 | self.side = side 324 | self.client_order_id = client_order_id 325 | self.order_configuration = OrderConfiguration( 326 | **order_configuration) if order_configuration is not None else None 327 | 328 | self.user_id = user_id 329 | self.status = status 330 | self.time_in_force = time_in_force 331 | self.created_time = datetime.strptime( 332 | created_time if len(created_time) <= 27 else 333 | created_time[:26]+'Z', "%Y-%m-%dT%H:%M:%S.%fZ") if created_time is not None else None 334 | self.completion_percentage = completion_percentage 335 | self.filled_size = filled_size 336 | self.average_filled_price = average_filled_price 337 | self.fee = fee 338 | self.number_of_fills = number_of_fills 339 | self.filled_value = filled_value 340 | self.pending_cancel = pending_cancel 341 | self.size_in_quote = size_in_quote 342 | self.total_fees = total_fees 343 | self.size_inclusive_of_fees = size_inclusive_of_fees 344 | self.total_value_after_fees = total_value_after_fees 345 | self.trigger_status = trigger_status 346 | self.order_type = order_type 347 | self.reject_reason = reject_reason 348 | self.settled = settled 349 | self.product_type = product_type 350 | self.reject_message = reject_message 351 | self.cancel_message = cancel_message 352 | self.order_placement_source = order_placement_source 353 | self.outstanding_hold_amount = outstanding_hold_amount 354 | 355 | self.is_liquidation = is_liquidation 356 | self.last_fill_time = last_fill_time 357 | self.edit_history = edit_history if edit_history is not None else None 358 | self.leverage = leverage 359 | self.margin_type = margin_type 360 | 361 | self.order_error = OrderError( 362 | **order_error) if order_error is not None else None 363 | 364 | self.kwargs = kwargs 365 | 366 | @classmethod 367 | def from_create_order_response(cls, response: requests.Response) -> 'Order': 368 | """ 369 | Factory method from the `create_order` response object. 370 | """ 371 | 372 | if not response.ok: 373 | raise CoinbaseAdvancedTradeAPIError.not_ok_response(response) 374 | 375 | result = response.json() 376 | 377 | if not result['success']: 378 | error_response = result['error_response'] 379 | return cls( 380 | None, None, None, None, None, None, None, None, None, 381 | None, None, None, None, None, None, None, None, None, 382 | None, None, None, None, None, None, None, None, None, 383 | None, None, None, None, None, None, None, error_response) 384 | 385 | success_response = result['success_response'] 386 | order_configuration = result['order_configuration'] 387 | return cls(**success_response, order_configuration=order_configuration) 388 | 389 | @classmethod 390 | def from_get_order_response(cls, response: requests.Response) -> 'Order': 391 | """ 392 | Factory method for creation from the `get_order` response object. 393 | """ 394 | 395 | if not response.ok: 396 | raise CoinbaseAdvancedTradeAPIError.not_ok_response(response) 397 | 398 | result = response.json() 399 | 400 | order = result['order'] 401 | 402 | return cls(**order) 403 | 404 | 405 | class OrdersPage(BaseModel): 406 | """ 407 | Orders page. 408 | """ 409 | 410 | orders: List[Order] 411 | has_next: bool 412 | cursor: Optional[str] 413 | sequence: int 414 | 415 | def __init__(self, 416 | orders: List[dict], 417 | has_next: bool, 418 | cursor: Optional[str], 419 | sequence: int, **kwargs 420 | ) -> None: 421 | 422 | self.orders = list(map(lambda x: Order(**x), orders) 423 | ) if orders is not None else [] 424 | 425 | self.has_next = has_next 426 | self.cursor = cursor 427 | self.sequence = sequence 428 | 429 | self.kwargs = kwargs 430 | 431 | @classmethod 432 | def from_response(cls, response: requests.Response) -> 'OrdersPage': 433 | """ 434 | Factory Method. 435 | """ 436 | 437 | if not response.ok: 438 | raise CoinbaseAdvancedTradeAPIError.not_ok_response(response) 439 | 440 | result = response.json() 441 | return cls(**result) 442 | 443 | def __iter__(self): 444 | return self.orders.__iter__() 445 | 446 | 447 | class OrderEdit(BaseModel): 448 | """ 449 | Order edit. 450 | """ 451 | 452 | success: bool 453 | errors: Optional[List[dict]] 454 | edit_failure_reason: Optional[str] 455 | preview_failure_reason: Optional[str] 456 | 457 | def __init__(self, success: bool, errors: Optional[List[dict]] = None, edit_failure_reason: Optional[str] = None, preview_failure_reason: Optional[str] = None, **kwargs) -> None: 458 | self.success = success 459 | self.errors = errors 460 | self.edit_failure_reason = edit_failure_reason 461 | self.preview_failure_reason = preview_failure_reason 462 | 463 | self.kwargs = kwargs 464 | 465 | @classmethod 466 | def from_response(cls, response: requests.Response) -> 'OrderEdit': 467 | """ 468 | Factory Method. 469 | """ 470 | 471 | if not response.ok: 472 | raise CoinbaseAdvancedTradeAPIError.not_ok_response(response) 473 | 474 | result = response.json() 475 | return cls(**result) 476 | 477 | 478 | class OrderEditPreview(BaseModel): 479 | """ 480 | Order edit. 481 | """ 482 | 483 | errors: Optional[List[dict]] 484 | slippage: str 485 | order_total: str 486 | commission_total: str 487 | quote_size: str 488 | base_size: str 489 | best_bid: str 490 | best_ask: str 491 | average_filled_price: str 492 | 493 | def __init__(self, errors: Optional[List[dict]] = None, slippage: str = "", order_total: str = "", commission_total: str = "", quote_size: str = "", base_size: str = "", best_bid: str = "", best_ask: str = "", average_filled_price: str = "", **kwargs) -> None: 494 | self.errors = errors 495 | self.slippage = slippage 496 | self.order_total = order_total 497 | self.commission_total = commission_total 498 | self.quote_size = quote_size 499 | self.base_size = base_size 500 | self.best_bid = best_bid 501 | self.best_ask = best_ask 502 | self.average_filled_price = average_filled_price 503 | 504 | self.kwargs = kwargs 505 | 506 | @classmethod 507 | def from_response(cls, response: requests.Response) -> 'OrderEditPreview': 508 | """ 509 | Factory Method. 510 | """ 511 | 512 | if not response.ok: 513 | raise CoinbaseAdvancedTradeAPIError.not_ok_response(response) 514 | 515 | result = response.json() 516 | return cls(**result) 517 | 518 | 519 | class OrderCancellation(BaseModel): 520 | """ 521 | Order cancellation. 522 | """ 523 | 524 | success: bool 525 | failure_reason: str 526 | order_id: str 527 | 528 | def __init__(self, success: bool, failure_reason: str, order_id: str, **kwargs) -> None: 529 | self.success = success 530 | self.failure_reason = failure_reason 531 | self.order_id = order_id 532 | 533 | self.kwargs = kwargs 534 | 535 | 536 | class OrderBatchCancellation(BaseModel): 537 | """ 538 | Batch/Page of order cancellations. 539 | """ 540 | 541 | results: List[OrderCancellation] 542 | 543 | def __init__(self, results: List[OrderCancellation], **kwargs) -> None: 544 | self.results = results 545 | 546 | self.kwargs = kwargs 547 | 548 | @classmethod 549 | def from_response(cls, response: requests.Response) -> 'OrderBatchCancellation': 550 | """ 551 | Factory method. 552 | """ 553 | 554 | if not response.ok: 555 | raise CoinbaseAdvancedTradeAPIError.not_ok_response(response) 556 | 557 | result = response.json() 558 | 559 | return cls(**result) 560 | 561 | 562 | class Fill(BaseModel): 563 | """ 564 | Object representing an order filled. 565 | """ 566 | 567 | entry_id: str 568 | trade_id: str 569 | order_id: str 570 | trade_time: Optional[datetime] 571 | trade_type: str 572 | price: str 573 | size: str 574 | commission: str 575 | product_id: str 576 | sequence_timestamp: Optional[datetime] 577 | liquidity_indicator: str 578 | size_in_quote: bool 579 | user_id: str 580 | side: str 581 | 582 | def __init__( 583 | self, 584 | entry_id: str, 585 | trade_id: str, 586 | order_id: str, 587 | trade_time: str, 588 | trade_type: str, 589 | price: str, 590 | size: str, 591 | commission: str, 592 | product_id: str, 593 | sequence_timestamp: str, 594 | liquidity_indicator: str, 595 | size_in_quote: bool, 596 | user_id: str, 597 | side: str, **kwargs) -> None: 598 | self.entry_id = entry_id 599 | self.trade_id = trade_id 600 | self.order_id = order_id 601 | self.trade_time = datetime.strptime( 602 | trade_time if len(trade_time) <= 27 else trade_time[:26]+'Z', 603 | "%Y-%m-%dT%H:%M:%S.%fZ") if trade_time is not None else None 604 | self.trade_type = trade_type 605 | self.price = price 606 | self.size = size 607 | self.commission = commission 608 | self.product_id = product_id 609 | self.sequence_timestamp = datetime.strptime( 610 | sequence_timestamp if len( 611 | sequence_timestamp) <= 27 else sequence_timestamp[:26] + 'Z', 612 | "%Y-%m-%dT%H:%M:%S.%fZ") if sequence_timestamp is not None else None 613 | self.liquidity_indicator = liquidity_indicator 614 | self.size_in_quote = size_in_quote 615 | self.user_id = user_id 616 | self.side = side 617 | 618 | self.kwargs = kwargs 619 | 620 | 621 | class FillsPage(BaseModel): 622 | """ 623 | Page of orders filled. 624 | """ 625 | 626 | fills: List[Fill] 627 | cursor: Optional[str] 628 | 629 | def __init__(self, 630 | fills: List[dict], 631 | cursor: Optional[str], **kwargs 632 | ) -> None: 633 | 634 | self.fills = list(map(lambda x: Fill(**x), fills) 635 | ) if fills is not None else [] 636 | 637 | self.cursor = cursor 638 | 639 | self.kwargs = kwargs 640 | 641 | @classmethod 642 | def from_response(cls, response: requests.Response) -> 'FillsPage': 643 | """ 644 | Factory Method. 645 | """ 646 | 647 | if not response.ok: 648 | raise CoinbaseAdvancedTradeAPIError.not_ok_response(response) 649 | 650 | result = response.json() 651 | return cls(**result) 652 | 653 | def __iter__(self): 654 | return self.fills.__iter__() 655 | --------------------------------------------------------------------------------