├── .flake8 ├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── build.yml ├── .gitignore ├── .isort.cfg ├── .pre-commit-config.yaml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── bfxapi ├── __init__.py ├── _client.py ├── _utils │ ├── __init__.py │ ├── json_decoder.py │ ├── json_encoder.py │ └── logging.py ├── _version.py ├── exceptions.py ├── py.typed ├── rest │ ├── __init__.py │ ├── _bfx_rest_interface.py │ ├── _interface │ │ ├── __init__.py │ │ ├── interface.py │ │ └── middleware.py │ ├── _interfaces │ │ ├── __init__.py │ │ ├── rest_auth_endpoints.py │ │ ├── rest_merchant_endpoints.py │ │ └── rest_public_endpoints.py │ └── exceptions.py ├── types │ ├── __init__.py │ ├── dataclasses.py │ ├── labeler.py │ ├── notification.py │ └── serializers.py └── websocket │ ├── __init__.py │ ├── _client │ ├── __init__.py │ ├── bfx_websocket_bucket.py │ ├── bfx_websocket_client.py │ └── bfx_websocket_inputs.py │ ├── _connection.py │ ├── _event_emitter │ ├── __init__.py │ └── bfx_event_emitter.py │ ├── _handlers │ ├── __init__.py │ ├── auth_events_handler.py │ └── public_channels_handler.py │ ├── exceptions.py │ └── subscriptions.py ├── dev-requirements.txt ├── examples ├── rest │ ├── auth │ │ ├── claim_position.py │ │ ├── get_wallets.py │ │ ├── set_derivative_position_collateral.py │ │ ├── submit_funding_offer.py │ │ ├── submit_order.py │ │ └── toggle_keep_funding.py │ ├── merchant │ │ ├── settings.py │ │ └── submit_invoice.py │ └── public │ │ ├── book.py │ │ ├── conf.py │ │ ├── get_candles_hist.py │ │ ├── pulse_endpoints.py │ │ ├── rest_calculation_endpoints.py │ │ └── trades.py └── websocket │ ├── auth │ ├── calc.py │ ├── submit_order.py │ └── wallets.py │ └── public │ ├── derivatives_status.py │ ├── order_book.py │ ├── raw_order_book.py │ ├── ticker.py │ └── trades.py ├── pyproject.toml ├── requirements.txt └── setup.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 80 3 | extend-select = B950 4 | extend-ignore = E203,E501,E701 5 | 6 | exclude = 7 | __pycache__ 8 | build 9 | dist 10 | venv 11 | 12 | per-file-ignores = 13 | */__init__.py:F401 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## I'm submitting a... 2 | 3 | - [ ] bug report; 4 | - [ ] feature request; 5 | - [ ] documentation change; 6 | 7 | ## What is the expected behaviour? 8 | 9 | 10 | ## What is the current behaviour? 11 | 12 | 13 | ## Possible solution (optional) 14 | 15 | 16 | 17 | 18 | A possible solution could be... 19 | 20 | ## Steps to reproduce (for bugs) 21 | 22 | 23 | 1. 24 | 2. 25 | 3. 26 | 27 | ### Python version 28 | 29 | 30 | Python 3.10.6 x64 31 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | 4 | ## Motivation and Context 5 | 6 | 7 | ## Related Issue 8 | 9 | 10 | 11 | PR fixes the following issue: 12 | 13 | ## Type of change 14 | 15 | 16 | - [ ] Bug fix (non-breaking change which fixes an issue); 17 | - [ ] New feature (non-breaking change which adds functionality); 18 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected); 19 | - [ ] This change requires a documentation update; 20 | 21 | # Checklist: 22 | 23 | - [ ] I've done a self-review of my code; 24 | - [ ] I've made corresponding changes to the documentation; 25 | - [ ] I've made sure my changes generate no warnings; 26 | - [ ] mypy returns no errors when run on the root package; 27 | 28 | - [ ] I've run black to format my code; 29 | - [ ] I've run isort to format my code's import statements; 30 | - [ ] flake8 reports no errors when run on the entire code base; 31 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: Set up Python 3.8 18 | uses: actions/setup-python@v4 19 | with: 20 | python-version: '3.8' 21 | - name: Install bitfinex-api-py's dependencies 22 | run: python -m pip install -r dev-requirements.txt 23 | - name: Run pre-commit hooks (see .pre-commit-config.yaml) 24 | uses: pre-commit/action@v3.0.1 25 | - name: Run mypy to ensure correct type hinting 26 | run: python -m mypy bfxapi 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .venv 2 | .DS_Store 3 | .vscode 4 | .python-version 5 | __pycache__ 6 | 7 | bitfinex_api_py.egg-info 8 | bitfinex_api_py.dist-info 9 | build/ 10 | dist/ 11 | pip-wheel-metadata/ 12 | .eggs 13 | 14 | .idea 15 | 16 | venv/ 17 | -------------------------------------------------------------------------------- /.isort.cfg: -------------------------------------------------------------------------------- 1 | [settings] 2 | profile = black 3 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/PyCQA/isort 3 | rev: 5.13.2 4 | hooks: 5 | - id: isort 6 | - repo: https://github.com/psf/black-pre-commit-mirror 7 | rev: 24.2.0 8 | hooks: 9 | - id: black 10 | - repo: https://github.com/PyCQA/flake8 11 | rev: 7.0.0 12 | hooks: 13 | - id: flake8 14 | 15 | additional_dependencies: [ 16 | flake8-bugbear 17 | ] 18 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | support@bitfinex.com (Bitfinex). 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bitfinex-api-py 2 | 3 | [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/bitfinex-api-py)](https://pypi.org/project/bitfinex-api-py/) 4 | [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) 5 | ![GitHub Action](https://github.com/bitfinexcom/bitfinex-api-py/actions/workflows/build.yml/badge.svg) 6 | 7 | Official implementation of the [Bitfinex APIs (V2)](https://docs.bitfinex.com/docs) for `Python 3.8+`. 8 | 9 | ### Features 10 | 11 | * Support for 75+ REST endpoints (a list of available endpoints can be found [here](https://docs.bitfinex.com/reference)) 12 | * New WebSocket client to ensure fast, secure and persistent connections 13 | * Full support for Bitfinex notifications (including custom notifications) 14 | * Native support for type hinting and type checking with [`mypy`](https://github.com/python/mypy) 15 | 16 | ## Installation 17 | 18 | ```console 19 | python3 -m pip install bitfinex-api-py 20 | ``` 21 | 22 | If you intend to use mypy type hints in your project, use: 23 | ```console 24 | python3 -m pip install bitfinex-api-py[typing] 25 | ``` 26 | 27 | --- 28 | 29 | # Quickstart 30 | 31 | ```python 32 | from bfxapi import Client, REST_HOST 33 | 34 | from bfxapi.types import Notification, Order 35 | 36 | bfx = Client( 37 | rest_host=REST_HOST, 38 | api_key="", 39 | api_secret="" 40 | ) 41 | 42 | notification: Notification[Order] = bfx.rest.auth.submit_order( 43 | type="EXCHANGE LIMIT", symbol="tBTCUSD", amount=0.165212, price=30264.0) 44 | 45 | order: Order = notification.data 46 | 47 | if notification.status == "SUCCESS": 48 | print(f"Successful new order for {order.symbol} at {order.price}$.") 49 | 50 | if notification.status == "ERROR": 51 | raise Exception(f"Something went wrong: {notification.text}") 52 | ``` 53 | 54 | ## Authenticating in your account 55 | 56 | To authenticate in your account, you must provide a valid API-KEY and API-SECRET: 57 | ```python 58 | bfx = Client( 59 | [...], 60 | api_key=os.getenv("BFX_API_KEY"), 61 | api_secret=os.getenv("BFX_API_SECRET") 62 | ) 63 | ``` 64 | 65 | ### Warning 66 | 67 | Remember to not share your API-KEYs and API-SECRETs with anyone. \ 68 | Everyone who owns one of your API-KEYs and API-SECRETs will have full access to your account. \ 69 | We suggest saving your credentials in a local `.env` file and accessing them as environment variables. 70 | 71 | _Revoke your API-KEYs and API-SECRETs immediately if you think they might have been stolen._ 72 | 73 | > **NOTE:** A guide on how to create, edit and revoke API-KEYs and API-SECRETs can be found [here](https://support.bitfinex.com/hc/en-us/articles/115003363429-How-to-create-and-revoke-a-Bitfinex-API-Key). 74 | 75 | ## Next 76 | 77 | * [WebSocket client documentation](#websocket-client-documentation) 78 | - [Advanced features](#advanced-features) 79 | - [Examples](#examples) 80 | * [How to contribute](#how-to-contribute) 81 | 82 | --- 83 | 84 | # WebSocket client documentation 85 | 86 | 1. [Instantiating the client](#instantiating-the-client) 87 | * [Authentication](#authentication) 88 | 2. [Running the client](#running-the-client) 89 | * [Closing the connection](#closing-the-connection) 90 | 3. [Subscribing to public channels](#subscribing-to-public-channels) 91 | * [Unsubscribing from a public channel](#unsubscribing-from-a-public-channel) 92 | * [Setting a custom `sub_id`](#setting-a-custom-sub_id) 93 | 4. [Listening to events](#listening-to-events) 94 | 95 | ### Advanced features 96 | * [Using custom notifications](#using-custom-notifications) 97 | 98 | ### Examples 99 | * [Creating a new order](#creating-a-new-order) 100 | 101 | ## Instantiating the client 102 | 103 | ```python 104 | bfx = Client(wss_host=PUB_WSS_HOST) 105 | ``` 106 | 107 | `Client::wss` contains an instance of `BfxWebSocketClient` (core implementation of the WebSocket client). \ 108 | The `wss_host` argument is used to indicate the URL to which the WebSocket client should connect. \ 109 | The `bfxapi` package exports 2 constants to quickly set this URL: 110 | 111 | Constant | URL | When to use 112 | :--- | :--- | :--- 113 | WSS_HOST | wss://api.bitfinex.com/ws/2 | Suitable for all situations, supports authentication. 114 | PUB_WSS_HOST | wss://api-pub.bitfinex.com/ws/2 | For public uses only, doesn't support authentication. 115 | 116 | PUB_WSS_HOST is recommended over WSS_HOST for applications that don't require authentication. 117 | 118 | > **NOTE:** The `wss_host` parameter is optional, and the default value is WSS_HOST. 119 | 120 | ### Authentication 121 | 122 | To learn how to authenticate in your account, have a look at [Authenticating in your account](#authenticating-in-your-account). 123 | 124 | If authentication is successful, the client will emit the `authenticated` event. \ 125 | All operations that require authentication will fail if run before the emission of this event. \ 126 | The `data` argument contains information about the authentication, such as the `userId`, the `auth_id`, etc... 127 | 128 | ```python 129 | @bfx.wss.on("authenticated") 130 | def on_authenticated(data: Dict[str, Any]): 131 | print(f"Successful login for user <{data['userId']}>.") 132 | ``` 133 | 134 | `data` can also be useful for checking if an API-KEY has certain permissions: 135 | 136 | ```python 137 | @bfx.wss.on("authenticated") 138 | def on_authenticated(data: Dict[str, Any]): 139 | if not data["caps"]["orders"]["read"]: 140 | raise Exception("This application requires read permissions on orders.") 141 | 142 | if not data["caps"]["positions"]["write"]: 143 | raise Exception("This application requires write permissions on positions.") 144 | ``` 145 | 146 | ## Running the client 147 | 148 | The client can be run using `BfxWebSocketClient::run`: 149 | ```python 150 | bfx.wss.run() 151 | ``` 152 | 153 | If an event loop is already running, users can start the client with `BfxWebSocketClient::start`: 154 | ```python 155 | await bfx.wss.start() 156 | ``` 157 | 158 | If the client succeeds in connecting to the server, it will emit the `open` event. \ 159 | This is the right place for all bootstrap activities, such as subscribing to public channels. \ 160 | To learn more about events and public channels, see [Listening to events](#listening-to-events) and [Subscribing to public channels](#subscribing-to-public-channels). 161 | 162 | ```python 163 | @bfx.wss.on("open") 164 | async def on_open(): 165 | await bfx.wss.subscribe("ticker", symbol="tBTCUSD") 166 | ``` 167 | 168 | ### Closing the connection 169 | 170 | Users can close the connection with the WebSocket server using `BfxWebSocketClient::close`: 171 | ```python 172 | await bfx.wss.close() 173 | ``` 174 | 175 | A custom [close code number](https://www.iana.org/assignments/websocket/websocket.xhtml#close-code-number), along with a verbose reason, can be given as parameters: 176 | ```python 177 | await bfx.wss.close(code=1001, reason="Going Away") 178 | ``` 179 | 180 | After closing the connection, the client will emit the `disconnected` event: 181 | ```python 182 | @bfx.wss.on("disconnected") 183 | def on_disconnected(code: int, reason: str): 184 | if code == 1000 or code == 1001: 185 | print("Closing the connection without errors!") 186 | ``` 187 | 188 | ## Subscribing to public channels 189 | 190 | Users can subscribe to public channels using `BfxWebSocketClient::subscribe`: 191 | ```python 192 | await bfx.wss.subscribe("ticker", symbol="tBTCUSD") 193 | ``` 194 | 195 | On each successful subscription, the client will emit the `subscribed` event: 196 | ```python 197 | @bfx.wss.on("subscribed") 198 | def on_subscribed(subscription: subscriptions.Subscription): 199 | if subscription["channel"] == "ticker": 200 | print(f"{subscription['symbol']}: {subscription['sub_id']}") # tBTCUSD: f2757df2-7e11-4244-9bb7-a53b7343bef8 201 | ``` 202 | 203 | ### Unsubscribing from a public channel 204 | 205 | It is possible to unsubscribe from a public channel at any time. \ 206 | Unsubscribing from a public channel prevents the client from receiving any more data from it. \ 207 | This can be done using `BfxWebSocketClient::unsubscribe`, and passing the `sub_id` of the public channel you want to unsubscribe from: 208 | 209 | ```python 210 | await bfx.wss.unsubscribe(sub_id="f2757df2-7e11-4244-9bb7-a53b7343bef8") 211 | ``` 212 | 213 | ### Setting a custom `sub_id` 214 | 215 | The client generates a random `sub_id` for each subscription. \ 216 | These values must be unique, as the client uses them to identify subscriptions. \ 217 | However, it is possible to force this value by passing a custom `sub_id` to `BfxWebSocketClient::subscribe`: 218 | 219 | ```python 220 | await bfx.wss.subscribe("candles", key="trade:1m:tBTCUSD", sub_id="507f1f77bcf86cd799439011") 221 | ``` 222 | 223 | ## Listening to events 224 | 225 | Whenever the WebSocket client receives data, it will emit a specific event. \ 226 | Users can either ignore those events or listen for them by registering callback functions. \ 227 | These callback functions can also be asynchronous; in fact the client fully supports coroutines ([`asyncio`](https://docs.python.org/3/library/asyncio.html)). 228 | 229 | To add a listener for a specific event, users can use the decorator `BfxWebSocketClient::on`: 230 | ```python 231 | @bfx.wss.on("candles_update") 232 | def on_candles_update(sub: subscriptions.Candles, candle: Candle): 233 | print(f"Candle update for key <{sub['key']}>: {candle}") 234 | ``` 235 | 236 | The same can be done without using decorators: 237 | ```python 238 | bfx.wss.on("candles_update", callback=on_candles_update) 239 | ``` 240 | 241 | # Advanced features 242 | 243 | ## Using custom notifications 244 | 245 | **Using custom notifications requires user authentication.** 246 | 247 | Users can send custom notifications using `BfxWebSocketClient::notify`: 248 | ```python 249 | await bfx.wss.notify({ "foo": 1 }) 250 | ``` 251 | 252 | Any data can be sent along with a custom notification. 253 | 254 | Custom notifications are broadcast by the server on all user's open connections. \ 255 | So, each custom notification will be sent to every online client of the current user. \ 256 | Whenever a client receives a custom notification, it will emit the `notification` event: 257 | ```python 258 | @bfx.wss.on("notification") 259 | def on_notification(notification: Notification[Any]): 260 | print(notification.data) # { "foo": 1 } 261 | ``` 262 | 263 | # Examples 264 | 265 | ## Creating a new order 266 | 267 | ```python 268 | import os 269 | 270 | from bfxapi import Client, WSS_HOST 271 | 272 | from bfxapi.types import Notification, Order 273 | 274 | bfx = Client( 275 | wss_host=WSS_HOST, 276 | api_key=os.getenv("BFX_API_KEY"), 277 | api_secret=os.getenv("BFX_API_SECRET") 278 | ) 279 | 280 | @bfx.wss.on("authenticated") 281 | async def on_authenticated(_): 282 | await bfx.wss.inputs.submit_order( 283 | type="EXCHANGE LIMIT", symbol="tBTCUSD", amount=0.165212, price=30264.0) 284 | 285 | @bfx.wss.on("order_new") 286 | def on_order_new(order: Order): 287 | print(f"Successful new order for {order.symbol} at {order.price}$.") 288 | 289 | @bfx.wss.on("on-req-notification") 290 | def on_notification(notification: Notification[Order]): 291 | if notification.status == "ERROR": 292 | raise Exception(f"Something went wrong: {notification.text}") 293 | 294 | bfx.wss.run() 295 | ``` 296 | 297 | --- 298 | 299 | # How to contribute 300 | 301 | All contributions are welcome! :D 302 | 303 | A guide on how to install and set up `bitfinex-api-py`'s source code can be found [here](#installation-and-setup). \ 304 | Before opening any pull requests, please have a look at [Before Opening a PR](#before-opening-a-pr). \ 305 | Contributors must uphold the [Contributor Covenant code of conduct](https://github.com/bitfinexcom/bitfinex-api-py/blob/master/CODE_OF_CONDUCT.md). 306 | 307 | ### Index 308 | 309 | 1. [Installation and setup](#installation-and-setup) 310 | * [Cloning the repository](#cloning-the-repository) 311 | * [Installing the dependencies](#installing-the-dependencies) 312 | * [Set up the pre-commit hooks (optional)](#set-up-the-pre-commit-hooks-optional) 313 | 2. [Before opening a PR](#before-opening-a-pr) 314 | * [Tip](#tip) 315 | 3. [License](#license) 316 | 317 | ## Installation and setup 318 | 319 | A brief guide on how to install and set up the project in your Python 3.8+ environment. 320 | 321 | ### Cloning the repository 322 | 323 | ```console 324 | git clone https://github.com/bitfinexcom/bitfinex-api-py.git 325 | ``` 326 | 327 | ### Installing the dependencies 328 | 329 | ```console 330 | python3 -m pip install -r dev-requirements.txt 331 | ``` 332 | 333 | Make sure to install `dev-requirements.txt` (and not `requirements.txt`!). \ 334 | `dev-requirements.txt` will install all dependencies in `requirements.txt` plus any development dependency. \ 335 | dev-requirements includes [mypy](https://github.com/python/mypy), [black](https://github.com/psf/black), [isort](https://github.com/PyCQA/isort), [flake8](https://github.com/PyCQA/flake8), and [pre-commit](https://github.com/pre-commit/pre-commit) (more on these tools in later chapters). 336 | 337 | All done, your Python 3.8+ environment should now be able to run `bitfinex-api-py`'s source code. 338 | 339 | ### Set up the pre-commit hooks (optional) 340 | 341 | **Do not skip this paragraph if you intend to contribute to the project.** 342 | 343 | This repository includes a pre-commit configuration file that defines the following hooks: 344 | 1. [isort](https://github.com/PyCQA/isort) 345 | 2. [black](https://github.com/psf/black) 346 | 3. [flake8](https://github.com/PyCQA/flake8) 347 | 348 | To set up pre-commit use: 349 | ```console 350 | python3 -m pre-commit install 351 | ``` 352 | 353 | These will ensure that isort, black and flake8 are run on each git commit. 354 | 355 | [Visit this page to learn more about git hooks and pre-commit.](https://pre-commit.com/#introduction) 356 | 357 | #### Manually triggering the pre-commit hooks 358 | 359 | You can also manually trigger the execution of all hooks with: 360 | ```console 361 | python3 -m pre-commit run --all-files 362 | ``` 363 | 364 | ## Before opening a PR 365 | 366 | **We won't accept your PR or we'll request changes if the following requirements aren't met.** 367 | 368 | Wheter you're submitting a bug fix, a new feature or a documentation change, you should first discuss it in an issue. 369 | 370 | You must be able to check off all tasks listed in [PULL_REQUEST_TEMPLATE](https://raw.githubusercontent.com/bitfinexcom/bitfinex-api-py/master/.github/PULL_REQUEST_TEMPLATE.md) before opening a pull request. 371 | 372 | ### Tip 373 | 374 | Setting up the project's pre-commit hooks will help automate this process ([more](#set-up-the-pre-commit-hooks-optional)). 375 | 376 | ## License 377 | 378 | ``` 379 | Copyright 2023 Bitfinex 380 | 381 | Licensed under the Apache License, Version 2.0 (the "License"); 382 | you may not use this file except in compliance with the License. 383 | You may obtain a copy of the License at 384 | 385 | http://www.apache.org/licenses/LICENSE-2.0 386 | 387 | Unless required by applicable law or agreed to in writing, software 388 | distributed under the License is distributed on an "AS IS" BASIS, 389 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 390 | See the License for the specific language governing permissions and 391 | limitations under the License. 392 | ``` 393 | -------------------------------------------------------------------------------- /bfxapi/__init__.py: -------------------------------------------------------------------------------- 1 | from ._client import PUB_REST_HOST, PUB_WSS_HOST, REST_HOST, WSS_HOST, Client 2 | -------------------------------------------------------------------------------- /bfxapi/_client.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING, List, Optional 2 | 3 | from bfxapi._utils.logging import ColorLogger 4 | from bfxapi.exceptions import IncompleteCredentialError 5 | from bfxapi.rest import BfxRestInterface 6 | from bfxapi.websocket import BfxWebSocketClient 7 | 8 | if TYPE_CHECKING: 9 | from bfxapi.websocket._client.bfx_websocket_client import _Credentials 10 | 11 | REST_HOST = "https://api.bitfinex.com/v2" 12 | WSS_HOST = "wss://api.bitfinex.com/ws/2" 13 | 14 | PUB_REST_HOST = "https://api-pub.bitfinex.com/v2" 15 | PUB_WSS_HOST = "wss://api-pub.bitfinex.com/ws/2" 16 | 17 | 18 | class Client: 19 | def __init__( 20 | self, 21 | api_key: Optional[str] = None, 22 | api_secret: Optional[str] = None, 23 | *, 24 | rest_host: str = REST_HOST, 25 | wss_host: str = WSS_HOST, 26 | filters: Optional[List[str]] = None, 27 | timeout: Optional[int] = 60 * 15, 28 | log_filename: Optional[str] = None, 29 | ) -> None: 30 | credentials: Optional["_Credentials"] = None 31 | 32 | if api_key and api_secret: 33 | credentials = { 34 | "api_key": api_key, 35 | "api_secret": api_secret, 36 | "filters": filters, 37 | } 38 | elif api_key: 39 | raise IncompleteCredentialError( 40 | "You must provide both API-KEY and API-SECRET (missing API-KEY)." 41 | ) 42 | elif api_secret: 43 | raise IncompleteCredentialError( 44 | "You must provide both API-KEY and API-SECRET (missing API-SECRET)." 45 | ) 46 | 47 | self.rest = BfxRestInterface(rest_host, api_key, api_secret) 48 | 49 | logger = ColorLogger("bfxapi", level="INFO") 50 | 51 | if log_filename: 52 | logger.register(filename=log_filename) 53 | 54 | self.wss = BfxWebSocketClient( 55 | wss_host, credentials=credentials, timeout=timeout, logger=logger 56 | ) 57 | -------------------------------------------------------------------------------- /bfxapi/_utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitfinexcom/bitfinex-api-py/791c84591f354914784643eed8fd1ac92c98c536/bfxapi/_utils/__init__.py -------------------------------------------------------------------------------- /bfxapi/_utils/json_decoder.py: -------------------------------------------------------------------------------- 1 | import json 2 | import re 3 | from typing import Any, Dict 4 | 5 | 6 | def _to_snake_case(string: str) -> str: 7 | return re.sub(r"(? Any: 11 | return {_to_snake_case(key): value for key, value in data.items()} 12 | 13 | 14 | class JSONDecoder(json.JSONDecoder): 15 | def __init__(self, *args: Any, **kwargs: Any) -> None: 16 | super().__init__(*args, **kwargs, object_hook=_object_hook) 17 | -------------------------------------------------------------------------------- /bfxapi/_utils/json_encoder.py: -------------------------------------------------------------------------------- 1 | import json 2 | from decimal import Decimal 3 | from typing import Any, Dict, List, Union 4 | 5 | _ExtJSON = Union[ 6 | Dict[str, "_ExtJSON"], List["_ExtJSON"], bool, int, float, str, Decimal, None 7 | ] 8 | 9 | _StrictJSON = Union[Dict[str, "_StrictJSON"], List["_StrictJSON"], int, str, None] 10 | 11 | 12 | def _clear(dictionary: Dict[str, Any]) -> Dict[str, Any]: 13 | return {key: value for key, value in dictionary.items() if value is not None} 14 | 15 | 16 | def _adapter(data: _ExtJSON) -> _StrictJSON: 17 | if isinstance(data, bool): 18 | return int(data) 19 | if isinstance(data, float): 20 | return format(Decimal(repr(data)), "f") 21 | if isinstance(data, Decimal): 22 | return format(data, "f") 23 | 24 | if isinstance(data, list): 25 | return [_adapter(sub_data) for sub_data in data] 26 | if isinstance(data, dict): 27 | return _clear({key: _adapter(value) for key, value in data.items()}) 28 | 29 | return data 30 | 31 | 32 | class JSONEncoder(json.JSONEncoder): 33 | def encode(self, o: _ExtJSON) -> str: 34 | return super().encode(_adapter(o)) 35 | -------------------------------------------------------------------------------- /bfxapi/_utils/logging.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from copy import copy 3 | from logging import FileHandler, Formatter, Logger, LogRecord, StreamHandler 4 | from typing import TYPE_CHECKING, Literal, Optional 5 | 6 | if TYPE_CHECKING: 7 | _Level = Literal["NOTSET", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] 8 | 9 | _BLACK, _RED, _GREEN, _YELLOW, _BLUE, _MAGENTA, _CYAN, _WHITE = [ 10 | f"\033[0;{90 + i}m" for i in range(8) 11 | ] 12 | 13 | ( 14 | _BOLD_BLACK, 15 | _BOLD_RED, 16 | _BOLD_GREEN, 17 | _BOLD_YELLOW, 18 | _BOLD_BLUE, 19 | _BOLD_MAGENTA, 20 | _BOLD_CYAN, 21 | _BOLD_WHITE, 22 | ) = [f"\033[1;{90 + i}m" for i in range(8)] 23 | 24 | _NC = "\033[0m" 25 | 26 | 27 | class _ColorFormatter(Formatter): 28 | __LEVELS = { 29 | "INFO": _BLUE, 30 | "WARNING": _YELLOW, 31 | "ERROR": _RED, 32 | "CRITICAL": _BOLD_RED, 33 | "DEBUG": _BOLD_WHITE, 34 | } 35 | 36 | def format(self, record: LogRecord) -> str: 37 | _record = copy(record) 38 | _record.name = _MAGENTA + record.name + _NC 39 | _record.levelname = _ColorFormatter.__format_level(record.levelname) 40 | 41 | return super().format(_record) 42 | 43 | def formatTime(self, record: LogRecord, datefmt: Optional[str] = None) -> str: 44 | return _GREEN + super().formatTime(record, datefmt) + _NC 45 | 46 | @staticmethod 47 | def __format_level(level: str) -> str: 48 | return _ColorFormatter.__LEVELS[level] + level + _NC 49 | 50 | 51 | _FORMAT = "%(asctime)s %(name)s %(levelname)s %(message)s" 52 | 53 | _DATE_FORMAT = "%d-%m-%Y %H:%M:%S" 54 | 55 | 56 | class ColorLogger(Logger): 57 | __FORMATTER = Formatter(_FORMAT, _DATE_FORMAT) 58 | 59 | def __init__(self, name: str, level: "_Level" = "NOTSET") -> None: 60 | super().__init__(name, level) 61 | 62 | formatter = _ColorFormatter(_FORMAT, _DATE_FORMAT) 63 | 64 | handler = StreamHandler(stream=sys.stderr) 65 | handler.setFormatter(fmt=formatter) 66 | self.addHandler(hdlr=handler) 67 | 68 | def register(self, filename: str) -> None: 69 | handler = FileHandler(filename=filename) 70 | handler.setFormatter(fmt=ColorLogger.__FORMATTER) 71 | self.addHandler(hdlr=handler) 72 | -------------------------------------------------------------------------------- /bfxapi/_version.py: -------------------------------------------------------------------------------- 1 | __version__ = "3.0.5" 2 | -------------------------------------------------------------------------------- /bfxapi/exceptions.py: -------------------------------------------------------------------------------- 1 | class BfxBaseException(Exception): 2 | """ 3 | Base class for every custom exception thrown by bitfinex-api-py. 4 | """ 5 | 6 | 7 | class IncompleteCredentialError(BfxBaseException): 8 | pass 9 | 10 | 11 | class InvalidCredentialError(BfxBaseException): 12 | pass 13 | -------------------------------------------------------------------------------- /bfxapi/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitfinexcom/bitfinex-api-py/791c84591f354914784643eed8fd1ac92c98c536/bfxapi/py.typed -------------------------------------------------------------------------------- /bfxapi/rest/__init__.py: -------------------------------------------------------------------------------- 1 | from ._bfx_rest_interface import BfxRestInterface 2 | -------------------------------------------------------------------------------- /bfxapi/rest/_bfx_rest_interface.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from bfxapi.rest._interfaces import ( 4 | RestAuthEndpoints, 5 | RestMerchantEndpoints, 6 | RestPublicEndpoints, 7 | ) 8 | 9 | 10 | class BfxRestInterface: 11 | def __init__( 12 | self, host: str, api_key: Optional[str] = None, api_secret: Optional[str] = None 13 | ): 14 | self.auth = RestAuthEndpoints(host=host, api_key=api_key, api_secret=api_secret) 15 | 16 | self.merchant = RestMerchantEndpoints( 17 | host=host, api_key=api_key, api_secret=api_secret 18 | ) 19 | 20 | self.public = RestPublicEndpoints(host=host) 21 | -------------------------------------------------------------------------------- /bfxapi/rest/_interface/__init__.py: -------------------------------------------------------------------------------- 1 | from .interface import Interface 2 | -------------------------------------------------------------------------------- /bfxapi/rest/_interface/interface.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from .middleware import Middleware 4 | 5 | 6 | class Interface: 7 | def __init__( 8 | self, host: str, api_key: Optional[str] = None, api_secret: Optional[str] = None 9 | ): 10 | self._m = Middleware(host, api_key, api_secret) 11 | -------------------------------------------------------------------------------- /bfxapi/rest/_interface/middleware.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import hmac 3 | import json 4 | from datetime import datetime 5 | from enum import IntEnum 6 | from typing import TYPE_CHECKING, Any, List, NoReturn, Optional 7 | 8 | import requests 9 | 10 | from bfxapi._utils.json_decoder import JSONDecoder 11 | from bfxapi._utils.json_encoder import JSONEncoder 12 | from bfxapi.exceptions import InvalidCredentialError 13 | from bfxapi.rest.exceptions import GenericError, RequestParameterError 14 | 15 | if TYPE_CHECKING: 16 | from requests.sessions import _Params 17 | 18 | 19 | class _Error(IntEnum): 20 | ERR_UNK = 10000 21 | ERR_GENERIC = 10001 22 | ERR_PARAMS = 10020 23 | ERR_AUTH_FAIL = 10100 24 | 25 | 26 | class Middleware: 27 | __TIMEOUT = 30 28 | 29 | def __init__( 30 | self, host: str, api_key: Optional[str] = None, api_secret: Optional[str] = None 31 | ): 32 | self.__host = host 33 | 34 | self.__api_key = api_key 35 | 36 | self.__api_secret = api_secret 37 | 38 | def get(self, endpoint: str, params: Optional["_Params"] = None) -> Any: 39 | headers = {"Accept": "application/json"} 40 | 41 | if self.__api_key and self.__api_secret: 42 | headers = {**headers, **self.__get_authentication_headers(endpoint)} 43 | 44 | request = requests.get( 45 | url=f"{self.__host}/{endpoint}", 46 | params=params, 47 | headers=headers, 48 | timeout=Middleware.__TIMEOUT, 49 | ) 50 | 51 | data = request.json(cls=JSONDecoder) 52 | 53 | if isinstance(data, list) and len(data) > 0 and data[0] == "error": 54 | self.__handle_error(data) 55 | 56 | return data 57 | 58 | def post( 59 | self, 60 | endpoint: str, 61 | body: Optional[Any] = None, 62 | params: Optional["_Params"] = None, 63 | ) -> Any: 64 | _body = body and json.dumps(body, cls=JSONEncoder) or None 65 | 66 | headers = {"Accept": "application/json", "Content-Type": "application/json"} 67 | 68 | if self.__api_key and self.__api_secret: 69 | headers = { 70 | **headers, 71 | **self.__get_authentication_headers(endpoint, _body), 72 | } 73 | 74 | request = requests.post( 75 | url=f"{self.__host}/{endpoint}", 76 | data=_body, 77 | params=params, 78 | headers=headers, 79 | timeout=Middleware.__TIMEOUT, 80 | ) 81 | 82 | data = request.json(cls=JSONDecoder) 83 | 84 | if isinstance(data, list) and len(data) > 0 and data[0] == "error": 85 | self.__handle_error(data) 86 | 87 | return data 88 | 89 | def __handle_error(self, error: List[Any]) -> NoReturn: 90 | if error[1] == _Error.ERR_PARAMS: 91 | raise RequestParameterError( 92 | "The request was rejected with the following parameter " 93 | f"error: <{error[2]}>." 94 | ) 95 | 96 | if error[1] == _Error.ERR_AUTH_FAIL: 97 | raise InvalidCredentialError( 98 | "Can't authenticate with given API-KEY and API-SECRET." 99 | ) 100 | 101 | if not error[1] or error[1] == _Error.ERR_UNK or error[1] == _Error.ERR_GENERIC: 102 | raise GenericError( 103 | "The request was rejected with the following generic " 104 | f"error: <{error[2]}>." 105 | ) 106 | 107 | raise RuntimeError( 108 | f"The request was rejected with an unexpected error: <{error}>." 109 | ) 110 | 111 | def __get_authentication_headers(self, endpoint: str, data: Optional[str] = None): 112 | assert self.__api_key and self.__api_secret 113 | 114 | nonce = str(round(datetime.now().timestamp() * 1_000_000)) 115 | 116 | if not data: 117 | message = f"/api/v2/{endpoint}{nonce}" 118 | else: 119 | message = f"/api/v2/{endpoint}{nonce}{data}" 120 | 121 | signature = hmac.new( 122 | key=self.__api_secret.encode("utf8"), 123 | msg=message.encode("utf8"), 124 | digestmod=hashlib.sha384, 125 | ) 126 | 127 | return { 128 | "bfx-nonce": nonce, 129 | "bfx-signature": signature.hexdigest(), 130 | "bfx-apikey": self.__api_key, 131 | } 132 | -------------------------------------------------------------------------------- /bfxapi/rest/_interfaces/__init__.py: -------------------------------------------------------------------------------- 1 | from .rest_auth_endpoints import RestAuthEndpoints 2 | from .rest_merchant_endpoints import RestMerchantEndpoints 3 | from .rest_public_endpoints import RestPublicEndpoints 4 | -------------------------------------------------------------------------------- /bfxapi/rest/_interfaces/rest_auth_endpoints.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | from typing import Any, Dict, List, Literal, Optional, Tuple, Union 3 | 4 | from bfxapi.rest._interface import Interface 5 | from bfxapi.types import ( 6 | BalanceAvailable, 7 | BaseMarginInfo, 8 | DepositAddress, 9 | DerivativePositionCollateral, 10 | DerivativePositionCollateralLimits, 11 | FundingAutoRenew, 12 | FundingCredit, 13 | FundingInfo, 14 | FundingLoan, 15 | FundingOffer, 16 | FundingTrade, 17 | Ledger, 18 | LightningNetworkInvoice, 19 | LoginHistory, 20 | Movement, 21 | Notification, 22 | Order, 23 | OrderTrade, 24 | Position, 25 | PositionAudit, 26 | PositionClaim, 27 | PositionHistory, 28 | PositionIncrease, 29 | PositionIncreaseInfo, 30 | PositionSnapshot, 31 | SymbolMarginInfo, 32 | Trade, 33 | Transfer, 34 | UserInfo, 35 | Wallet, 36 | Withdrawal, 37 | serializers, 38 | ) 39 | from bfxapi.types.serializers import _Notification 40 | 41 | 42 | class RestAuthEndpoints(Interface): 43 | def get_user_info(self) -> UserInfo: 44 | return serializers.UserInfo.parse(*self._m.post("auth/r/info/user")) 45 | 46 | def get_login_history(self) -> List[LoginHistory]: 47 | return [ 48 | serializers.LoginHistory.parse(*sub_data) 49 | for sub_data in self._m.post("auth/r/logins/hist") 50 | ] 51 | 52 | def get_balance_available_for_orders_or_offers( 53 | self, 54 | symbol: str, 55 | type: str, 56 | *, 57 | dir: Optional[int] = None, 58 | rate: Optional[str] = None, 59 | lev: Optional[str] = None, 60 | ) -> BalanceAvailable: 61 | body = {"symbol": symbol, "type": type, "dir": dir, "rate": rate, "lev": lev} 62 | 63 | return serializers.BalanceAvailable.parse( 64 | *self._m.post("auth/calc/order/avail", body=body) 65 | ) 66 | 67 | def get_wallets(self) -> List[Wallet]: 68 | return [ 69 | serializers.Wallet.parse(*sub_data) 70 | for sub_data in self._m.post("auth/r/wallets") 71 | ] 72 | 73 | def get_orders( 74 | self, *, symbol: Optional[str] = None, ids: Optional[List[str]] = None 75 | ) -> List[Order]: 76 | if symbol is None: 77 | endpoint = "auth/r/orders" 78 | else: 79 | endpoint = f"auth/r/orders/{symbol}" 80 | 81 | return [ 82 | serializers.Order.parse(*sub_data) 83 | for sub_data in self._m.post(endpoint, body={"id": ids}) 84 | ] 85 | 86 | def submit_order( 87 | self, 88 | type: str, 89 | symbol: str, 90 | amount: Union[str, float, Decimal], 91 | price: Union[str, float, Decimal], 92 | *, 93 | lev: Optional[int] = None, 94 | price_trailing: Optional[Union[str, float, Decimal]] = None, 95 | price_aux_limit: Optional[Union[str, float, Decimal]] = None, 96 | price_oco_stop: Optional[Union[str, float, Decimal]] = None, 97 | gid: Optional[int] = None, 98 | cid: Optional[int] = None, 99 | flags: Optional[int] = None, 100 | tif: Optional[str] = None, 101 | meta: Optional[Dict[str, Any]] = None, 102 | ) -> Notification[Order]: 103 | body = { 104 | "type": type, 105 | "symbol": symbol, 106 | "amount": amount, 107 | "price": price, 108 | "lev": lev, 109 | "price_trailing": price_trailing, 110 | "price_aux_limit": price_aux_limit, 111 | "price_oco_stop": price_oco_stop, 112 | "gid": gid, 113 | "cid": cid, 114 | "flags": flags, 115 | "tif": tif, 116 | "meta": meta, 117 | } 118 | 119 | return _Notification[Order](serializers.Order).parse( 120 | *self._m.post("auth/w/order/submit", body=body) 121 | ) 122 | 123 | def update_order( 124 | self, 125 | id: int, 126 | *, 127 | amount: Optional[Union[str, float, Decimal]] = None, 128 | price: Optional[Union[str, float, Decimal]] = None, 129 | cid: Optional[int] = None, 130 | cid_date: Optional[str] = None, 131 | gid: Optional[int] = None, 132 | flags: Optional[int] = None, 133 | lev: Optional[int] = None, 134 | delta: Optional[Union[str, float, Decimal]] = None, 135 | price_aux_limit: Optional[Union[str, float, Decimal]] = None, 136 | price_trailing: Optional[Union[str, float, Decimal]] = None, 137 | tif: Optional[str] = None, 138 | ) -> Notification[Order]: 139 | body = { 140 | "id": id, 141 | "amount": amount, 142 | "price": price, 143 | "cid": cid, 144 | "cid_date": cid_date, 145 | "gid": gid, 146 | "flags": flags, 147 | "lev": lev, 148 | "delta": delta, 149 | "price_aux_limit": price_aux_limit, 150 | "price_trailing": price_trailing, 151 | "tif": tif, 152 | } 153 | 154 | return _Notification[Order](serializers.Order).parse( 155 | *self._m.post("auth/w/order/update", body=body) 156 | ) 157 | 158 | def cancel_order( 159 | self, 160 | *, 161 | id: Optional[int] = None, 162 | cid: Optional[int] = None, 163 | cid_date: Optional[str] = None, 164 | ) -> Notification[Order]: 165 | return _Notification[Order](serializers.Order).parse( 166 | *self._m.post( 167 | "auth/w/order/cancel", body={"id": id, "cid": cid, "cid_date": cid_date} 168 | ) 169 | ) 170 | 171 | def cancel_order_multi( 172 | self, 173 | *, 174 | id: Optional[List[int]] = None, 175 | cid: Optional[List[Tuple[int, str]]] = None, 176 | gid: Optional[List[int]] = None, 177 | all: Optional[bool] = None, 178 | ) -> Notification[List[Order]]: 179 | body = {"id": id, "cid": cid, "gid": gid, "all": all} 180 | 181 | return _Notification[List[Order]](serializers.Order, is_iterable=True).parse( 182 | *self._m.post("auth/w/order/cancel/multi", body=body) 183 | ) 184 | 185 | def get_orders_history( 186 | self, 187 | *, 188 | symbol: Optional[str] = None, 189 | ids: Optional[List[int]] = None, 190 | start: Optional[str] = None, 191 | end: Optional[str] = None, 192 | limit: Optional[int] = None, 193 | ) -> List[Order]: 194 | if symbol is None: 195 | endpoint = "auth/r/orders/hist" 196 | else: 197 | endpoint = f"auth/r/orders/{symbol}/hist" 198 | 199 | body = {"id": ids, "start": start, "end": end, "limit": limit} 200 | 201 | return [ 202 | serializers.Order.parse(*sub_data) 203 | for sub_data in self._m.post(endpoint, body=body) 204 | ] 205 | 206 | def get_order_trades(self, symbol: str, id: int) -> List[OrderTrade]: 207 | return [ 208 | serializers.OrderTrade.parse(*sub_data) 209 | for sub_data in self._m.post(f"auth/r/order/{symbol}:{id}/trades") 210 | ] 211 | 212 | def get_trades_history( 213 | self, 214 | *, 215 | symbol: Optional[str] = None, 216 | sort: Optional[int] = None, 217 | start: Optional[str] = None, 218 | end: Optional[str] = None, 219 | limit: Optional[int] = None, 220 | ) -> List[Trade]: 221 | if symbol is None: 222 | endpoint = "auth/r/trades/hist" 223 | else: 224 | endpoint = f"auth/r/trades/{symbol}/hist" 225 | 226 | body = {"sort": sort, "start": start, "end": end, "limit": limit} 227 | 228 | return [ 229 | serializers.Trade.parse(*sub_data) 230 | for sub_data in self._m.post(endpoint, body=body) 231 | ] 232 | 233 | def get_ledgers( 234 | self, 235 | currency: Optional[str] = None, 236 | *, 237 | category: Optional[int] = None, 238 | start: Optional[str] = None, 239 | end: Optional[str] = None, 240 | limit: Optional[int] = None, 241 | ) -> List[Ledger]: 242 | if currency is None: 243 | endpoint = "auth/r/ledgers/hist" 244 | else: 245 | endpoint = f"auth/r/ledgers/{currency}/hist" 246 | 247 | body = {"category": category, "start": start, "end": end, "limit": limit} 248 | 249 | return [ 250 | serializers.Ledger.parse(*sub_data) 251 | for sub_data in self._m.post(endpoint, body=body) 252 | ] 253 | 254 | def get_base_margin_info(self) -> BaseMarginInfo: 255 | return serializers.BaseMarginInfo.parse( 256 | *self._m.post("auth/r/info/margin/base") 257 | ) 258 | 259 | def get_symbol_margin_info(self, symbol: str) -> SymbolMarginInfo: 260 | return serializers.SymbolMarginInfo.parse( 261 | *self._m.post(f"auth/r/info/margin/{symbol}") 262 | ) 263 | 264 | def get_all_symbols_margin_info(self) -> List[SymbolMarginInfo]: 265 | return [ 266 | serializers.SymbolMarginInfo.parse(*sub_data) 267 | for sub_data in self._m.post("auth/r/info/margin/sym_all") 268 | ] 269 | 270 | def get_positions(self) -> List[Position]: 271 | return [ 272 | serializers.Position.parse(*sub_data) 273 | for sub_data in self._m.post("auth/r/positions") 274 | ] 275 | 276 | def claim_position( 277 | self, id: int, *, amount: Optional[Union[str, float, Decimal]] = None 278 | ) -> Notification[PositionClaim]: 279 | return _Notification[PositionClaim](serializers.PositionClaim).parse( 280 | *self._m.post("auth/w/position/claim", body={"id": id, "amount": amount}) 281 | ) 282 | 283 | def increase_position( 284 | self, symbol: str, amount: Union[str, float, Decimal] 285 | ) -> Notification[PositionIncrease]: 286 | return _Notification[PositionIncrease](serializers.PositionIncrease).parse( 287 | *self._m.post( 288 | "auth/w/position/increase", body={"symbol": symbol, "amount": amount} 289 | ) 290 | ) 291 | 292 | def get_increase_position_info( 293 | self, symbol: str, amount: Union[str, float, Decimal] 294 | ) -> PositionIncreaseInfo: 295 | return serializers.PositionIncreaseInfo.parse( 296 | *self._m.post( 297 | "auth/r/position/increase/info", 298 | body={"symbol": symbol, "amount": amount}, 299 | ) 300 | ) 301 | 302 | def get_positions_history( 303 | self, 304 | *, 305 | start: Optional[str] = None, 306 | end: Optional[str] = None, 307 | limit: Optional[int] = None, 308 | ) -> List[PositionHistory]: 309 | return [ 310 | serializers.PositionHistory.parse(*sub_data) 311 | for sub_data in self._m.post( 312 | "auth/r/positions/hist", 313 | body={"start": start, "end": end, "limit": limit}, 314 | ) 315 | ] 316 | 317 | def get_positions_snapshot( 318 | self, 319 | *, 320 | start: Optional[str] = None, 321 | end: Optional[str] = None, 322 | limit: Optional[int] = None, 323 | ) -> List[PositionSnapshot]: 324 | return [ 325 | serializers.PositionSnapshot.parse(*sub_data) 326 | for sub_data in self._m.post( 327 | "auth/r/positions/snap", 328 | body={"start": start, "end": end, "limit": limit}, 329 | ) 330 | ] 331 | 332 | def get_positions_audit( 333 | self, 334 | *, 335 | ids: Optional[List[int]] = None, 336 | start: Optional[str] = None, 337 | end: Optional[str] = None, 338 | limit: Optional[int] = None, 339 | ) -> List[PositionAudit]: 340 | body = {"ids": ids, "start": start, "end": end, "limit": limit} 341 | 342 | return [ 343 | serializers.PositionAudit.parse(*sub_data) 344 | for sub_data in self._m.post("auth/r/positions/audit", body=body) 345 | ] 346 | 347 | def set_derivative_position_collateral( 348 | self, symbol: str, collateral: Union[str, float, Decimal] 349 | ) -> DerivativePositionCollateral: 350 | return serializers.DerivativePositionCollateral.parse( 351 | *( 352 | self._m.post( 353 | "auth/w/deriv/collateral/set", 354 | body={"symbol": symbol, "collateral": collateral}, 355 | )[0] 356 | ) 357 | ) 358 | 359 | def get_derivative_position_collateral_limits( 360 | self, symbol: str 361 | ) -> DerivativePositionCollateralLimits: 362 | return serializers.DerivativePositionCollateralLimits.parse( 363 | *self._m.post("auth/calc/deriv/collateral/limit", body={"symbol": symbol}) 364 | ) 365 | 366 | def get_funding_offers(self, *, symbol: Optional[str] = None) -> List[FundingOffer]: 367 | if symbol is None: 368 | endpoint = "auth/r/funding/offers" 369 | else: 370 | endpoint = f"auth/r/funding/offers/{symbol}" 371 | 372 | return [ 373 | serializers.FundingOffer.parse(*sub_data) 374 | for sub_data in self._m.post(endpoint) 375 | ] 376 | 377 | def submit_funding_offer( 378 | self, 379 | type: str, 380 | symbol: str, 381 | amount: Union[str, float, Decimal], 382 | rate: Union[str, float, Decimal], 383 | period: int, 384 | *, 385 | flags: Optional[int] = None, 386 | ) -> Notification[FundingOffer]: 387 | body = { 388 | "type": type, 389 | "symbol": symbol, 390 | "amount": amount, 391 | "rate": rate, 392 | "period": period, 393 | "flags": flags, 394 | } 395 | 396 | return _Notification[FundingOffer](serializers.FundingOffer).parse( 397 | *self._m.post("auth/w/funding/offer/submit", body=body) 398 | ) 399 | 400 | def cancel_funding_offer(self, id: int) -> Notification[FundingOffer]: 401 | return _Notification[FundingOffer](serializers.FundingOffer).parse( 402 | *self._m.post("auth/w/funding/offer/cancel", body={"id": id}) 403 | ) 404 | 405 | def cancel_all_funding_offers(self, currency: str) -> Notification[Literal[None]]: 406 | return _Notification[Literal[None]](None).parse( 407 | *self._m.post( 408 | "auth/w/funding/offer/cancel/all", body={"currency": currency} 409 | ) 410 | ) 411 | 412 | def submit_funding_close(self, id: int) -> Notification[Literal[None]]: 413 | return _Notification[Literal[None]](None).parse( 414 | *self._m.post("auth/w/funding/close", body={"id": id}) 415 | ) 416 | 417 | def toggle_auto_renew( 418 | self, 419 | status: bool, 420 | currency: str, 421 | *, 422 | amount: Optional[str] = None, 423 | rate: Optional[int] = None, 424 | period: Optional[int] = None, 425 | ) -> Notification[FundingAutoRenew]: 426 | body = { 427 | "status": status, 428 | "currency": currency, 429 | "amount": amount, 430 | "rate": rate, 431 | "period": period, 432 | } 433 | 434 | return _Notification[FundingAutoRenew](serializers.FundingAutoRenew).parse( 435 | *self._m.post("auth/w/funding/auto", body=body) 436 | ) 437 | 438 | def toggle_keep_funding( 439 | self, 440 | type: Literal["credit", "loan"], 441 | *, 442 | ids: Optional[List[int]] = None, 443 | changes: Optional[Dict[int, Literal[1, 2]]] = None, 444 | ) -> Notification[Literal[None]]: 445 | return _Notification[Literal[None]](None).parse( 446 | *self._m.post( 447 | "auth/w/funding/keep", 448 | body={"type": type, "id": ids, "changes": changes}, 449 | ) 450 | ) 451 | 452 | def get_funding_offers_history( 453 | self, 454 | *, 455 | symbol: Optional[str] = None, 456 | start: Optional[str] = None, 457 | end: Optional[str] = None, 458 | limit: Optional[int] = None, 459 | ) -> List[FundingOffer]: 460 | if symbol is None: 461 | endpoint = "auth/r/funding/offers/hist" 462 | else: 463 | endpoint = f"auth/r/funding/offers/{symbol}/hist" 464 | 465 | return [ 466 | serializers.FundingOffer.parse(*sub_data) 467 | for sub_data in self._m.post( 468 | endpoint, body={"start": start, "end": end, "limit": limit} 469 | ) 470 | ] 471 | 472 | def get_funding_loans(self, *, symbol: Optional[str] = None) -> List[FundingLoan]: 473 | if symbol is None: 474 | endpoint = "auth/r/funding/loans" 475 | else: 476 | endpoint = f"auth/r/funding/loans/{symbol}" 477 | 478 | return [ 479 | serializers.FundingLoan.parse(*sub_data) 480 | for sub_data in self._m.post(endpoint) 481 | ] 482 | 483 | def get_funding_loans_history( 484 | self, 485 | *, 486 | symbol: Optional[str] = None, 487 | start: Optional[str] = None, 488 | end: Optional[str] = None, 489 | limit: Optional[int] = None, 490 | ) -> List[FundingLoan]: 491 | if symbol is None: 492 | endpoint = "auth/r/funding/loans/hist" 493 | else: 494 | endpoint = f"auth/r/funding/loans/{symbol}/hist" 495 | 496 | return [ 497 | serializers.FundingLoan.parse(*sub_data) 498 | for sub_data in self._m.post( 499 | endpoint, body={"start": start, "end": end, "limit": limit} 500 | ) 501 | ] 502 | 503 | def get_funding_credits( 504 | self, *, symbol: Optional[str] = None 505 | ) -> List[FundingCredit]: 506 | if symbol is None: 507 | endpoint = "auth/r/funding/credits" 508 | else: 509 | endpoint = f"auth/r/funding/credits/{symbol}" 510 | 511 | return [ 512 | serializers.FundingCredit.parse(*sub_data) 513 | for sub_data in self._m.post(endpoint) 514 | ] 515 | 516 | def get_funding_credits_history( 517 | self, 518 | *, 519 | symbol: Optional[str] = None, 520 | start: Optional[str] = None, 521 | end: Optional[str] = None, 522 | limit: Optional[int] = None, 523 | ) -> List[FundingCredit]: 524 | if symbol is None: 525 | endpoint = "auth/r/funding/credits/hist" 526 | else: 527 | endpoint = f"auth/r/funding/credits/{symbol}/hist" 528 | 529 | return [ 530 | serializers.FundingCredit.parse(*sub_data) 531 | for sub_data in self._m.post( 532 | endpoint, body={"start": start, "end": end, "limit": limit} 533 | ) 534 | ] 535 | 536 | def get_funding_trades_history( 537 | self, 538 | *, 539 | symbol: Optional[str] = None, 540 | sort: Optional[int] = None, 541 | start: Optional[str] = None, 542 | end: Optional[str] = None, 543 | limit: Optional[int] = None, 544 | ) -> List[FundingTrade]: 545 | if symbol is None: 546 | endpoint = "auth/r/funding/trades/hist" 547 | else: 548 | endpoint = f"auth/r/funding/trades/{symbol}/hist" 549 | 550 | body = {"sort": sort, "start": start, "end": end, "limit": limit} 551 | 552 | return [ 553 | serializers.FundingTrade.parse(*sub_data) 554 | for sub_data in self._m.post(endpoint, body=body) 555 | ] 556 | 557 | def get_funding_info(self, key: str) -> FundingInfo: 558 | return serializers.FundingInfo.parse( 559 | *self._m.post(f"auth/r/info/funding/{key}") 560 | ) 561 | 562 | def transfer_between_wallets( 563 | self, 564 | from_wallet: str, 565 | to_wallet: str, 566 | currency: str, 567 | currency_to: str, 568 | amount: Union[str, float, Decimal], 569 | ) -> Notification[Transfer]: 570 | body = { 571 | "from": from_wallet, 572 | "to": to_wallet, 573 | "currency": currency, 574 | "currency_to": currency_to, 575 | "amount": amount, 576 | } 577 | 578 | return _Notification[Transfer](serializers.Transfer).parse( 579 | *self._m.post("auth/w/transfer", body=body) 580 | ) 581 | 582 | def submit_wallet_withdrawal( 583 | self, wallet: str, method: str, address: str, amount: Union[str, float, Decimal] 584 | ) -> Notification[Withdrawal]: 585 | body = { 586 | "wallet": wallet, 587 | "method": method, 588 | "address": address, 589 | "amount": amount, 590 | } 591 | 592 | return _Notification[Withdrawal](serializers.Withdrawal).parse( 593 | *self._m.post("auth/w/withdraw", body=body) 594 | ) 595 | 596 | def get_deposit_address( 597 | self, wallet: str, method: str, op_renew: bool = False 598 | ) -> Notification[DepositAddress]: 599 | return _Notification[DepositAddress](serializers.DepositAddress).parse( 600 | *self._m.post( 601 | "auth/w/deposit/address", 602 | body={"wallet": wallet, "method": method, "op_renew": op_renew}, 603 | ) 604 | ) 605 | 606 | def generate_deposit_invoice( 607 | self, wallet: str, currency: str, amount: Union[str, float, Decimal] 608 | ) -> LightningNetworkInvoice: 609 | return serializers.LightningNetworkInvoice.parse( 610 | *self._m.post( 611 | "auth/w/deposit/invoice", 612 | body={"wallet": wallet, "currency": currency, "amount": amount}, 613 | ) 614 | ) 615 | 616 | def get_movements( 617 | self, 618 | *, 619 | currency: Optional[str] = None, 620 | start: Optional[str] = None, 621 | end: Optional[str] = None, 622 | limit: Optional[int] = None, 623 | ) -> List[Movement]: 624 | if currency is None: 625 | endpoint = "auth/r/movements/hist" 626 | else: 627 | endpoint = f"auth/r/movements/{currency}/hist" 628 | 629 | return [ 630 | serializers.Movement.parse(*sub_data) 631 | for sub_data in self._m.post( 632 | endpoint, body={"start": start, "end": end, "limit": limit} 633 | ) 634 | ] 635 | -------------------------------------------------------------------------------- /bfxapi/rest/_interfaces/rest_merchant_endpoints.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | from typing import Any, Dict, List, Literal, Optional, Union 3 | 4 | from bfxapi.rest._interface import Interface 5 | from bfxapi.types import ( 6 | CurrencyConversion, 7 | InvoicePage, 8 | InvoiceStats, 9 | InvoiceSubmission, 10 | MerchantDeposit, 11 | MerchantUnlinkedDeposit, 12 | ) 13 | 14 | 15 | class RestMerchantEndpoints(Interface): 16 | def submit_invoice( 17 | self, 18 | amount: Union[str, float, Decimal], 19 | currency: str, 20 | order_id: str, 21 | customer_info: Dict[str, Any], 22 | pay_currencies: List[str], 23 | *, 24 | duration: Optional[int] = None, 25 | webhook: Optional[str] = None, 26 | redirect_url: Optional[str] = None, 27 | ) -> InvoiceSubmission: 28 | body = { 29 | "amount": amount, 30 | "currency": currency, 31 | "orderId": order_id, 32 | "customerInfo": customer_info, 33 | "payCurrencies": pay_currencies, 34 | "duration": duration, 35 | "webhook": webhook, 36 | "redirectUrl": redirect_url, 37 | } 38 | 39 | data = self._m.post("auth/w/ext/pay/invoice/create", body=body) 40 | 41 | return InvoiceSubmission.parse(data) 42 | 43 | def get_invoices( 44 | self, 45 | *, 46 | id: Optional[str] = None, 47 | start: Optional[str] = None, 48 | end: Optional[str] = None, 49 | limit: Optional[int] = None, 50 | ) -> List[InvoiceSubmission]: 51 | body = {"id": id, "start": start, "end": end, "limit": limit} 52 | 53 | data = self._m.post("auth/r/ext/pay/invoices", body=body) 54 | 55 | return [InvoiceSubmission.parse(sub_data) for sub_data in data] 56 | 57 | def get_invoices_paginated( 58 | self, 59 | page: int = 1, 60 | page_size: int = 10, 61 | sort: Literal["asc", "desc"] = "asc", 62 | sort_field: Literal["t", "amount", "status"] = "t", 63 | *, 64 | status: Optional[ 65 | List[Literal["CREATED", "PENDING", "COMPLETED", "EXPIRED"]] 66 | ] = None, 67 | fiat: Optional[List[str]] = None, 68 | crypto: Optional[List[str]] = None, 69 | id: Optional[str] = None, 70 | order_id: Optional[str] = None, 71 | ) -> InvoicePage: 72 | body = { 73 | "page": page, 74 | "pageSize": page_size, 75 | "sort": sort, 76 | "sortField": sort_field, 77 | "status": status, 78 | "fiat": fiat, 79 | "crypto": crypto, 80 | "id": id, 81 | "orderId": order_id, 82 | } 83 | 84 | data = self._m.post("auth/r/ext/pay/invoices/paginated", body=body) 85 | 86 | return InvoicePage.parse(data) 87 | 88 | def get_invoice_count_stats( 89 | self, status: Literal["CREATED", "PENDING", "COMPLETED", "EXPIRED"], format: str 90 | ) -> List[InvoiceStats]: 91 | return [ 92 | InvoiceStats(**sub_data) 93 | for sub_data in self._m.post( 94 | "auth/r/ext/pay/invoice/stats/count", 95 | body={"status": status, "format": format}, 96 | ) 97 | ] 98 | 99 | def get_invoice_earning_stats( 100 | self, currency: str, format: str 101 | ) -> List[InvoiceStats]: 102 | return [ 103 | InvoiceStats(**sub_data) 104 | for sub_data in self._m.post( 105 | "auth/r/ext/pay/invoice/stats/earning", 106 | body={"currency": currency, "format": format}, 107 | ) 108 | ] 109 | 110 | def complete_invoice( 111 | self, 112 | id: str, 113 | pay_currency: str, 114 | *, 115 | deposit_id: Optional[int] = None, 116 | ledger_id: Optional[int] = None, 117 | ) -> InvoiceSubmission: 118 | body = { 119 | "id": id, 120 | "payCcy": pay_currency, 121 | "depositId": deposit_id, 122 | "ledgerId": ledger_id, 123 | } 124 | 125 | data = self._m.post("auth/w/ext/pay/invoice/complete", body=body) 126 | 127 | return InvoiceSubmission.parse(data) 128 | 129 | def expire_invoice(self, id: str) -> InvoiceSubmission: 130 | body = {"id": id} 131 | 132 | data = self._m.post("auth/w/ext/pay/invoice/expire", body=body) 133 | 134 | return InvoiceSubmission.parse(data) 135 | 136 | def get_currency_conversion_list(self) -> List[CurrencyConversion]: 137 | return [ 138 | CurrencyConversion(**sub_data) 139 | for sub_data in self._m.post("auth/r/ext/pay/settings/convert/list") 140 | ] 141 | 142 | def add_currency_conversion(self, base_ccy: str, convert_ccy: str) -> bool: 143 | return bool( 144 | self._m.post( 145 | "auth/w/ext/pay/settings/convert/create", 146 | body={"baseCcy": base_ccy, "convertCcy": convert_ccy}, 147 | ) 148 | ) 149 | 150 | def remove_currency_conversion(self, base_ccy: str, convert_ccy: str) -> bool: 151 | return bool( 152 | self._m.post( 153 | "auth/w/ext/pay/settings/convert/remove", 154 | body={"baseCcy": base_ccy, "convertCcy": convert_ccy}, 155 | ) 156 | ) 157 | 158 | def set_merchant_settings(self, key: str, val: Any) -> bool: 159 | return bool( 160 | self._m.post("auth/w/ext/pay/settings/set", body={"key": key, "val": val}) 161 | ) 162 | 163 | def get_merchant_settings(self, key: str) -> Any: 164 | return self._m.post("auth/r/ext/pay/settings/get", body={"key": key}) 165 | 166 | def list_merchant_settings( 167 | self, keys: Optional[List[str]] = None 168 | ) -> Dict[str, Any]: 169 | return self._m.post("auth/r/ext/pay/settings/list", body={"keys": keys or []}) 170 | 171 | def get_deposits( 172 | self, 173 | start: int, 174 | to: int, 175 | *, 176 | ccy: Optional[str] = None, 177 | unlinked: Optional[bool] = None, 178 | ) -> List[MerchantDeposit]: 179 | body = {"from": start, "to": to, "ccy": ccy, "unlinked": unlinked} 180 | 181 | data = self._m.post("auth/r/ext/pay/deposits", body=body) 182 | 183 | return [MerchantDeposit(**sub_data) for sub_data in data] 184 | 185 | def get_unlinked_deposits( 186 | self, ccy: str, *, start: Optional[int] = None, end: Optional[int] = None 187 | ) -> List[MerchantUnlinkedDeposit]: 188 | body = {"ccy": ccy, "start": start, "end": end} 189 | 190 | data = self._m.post("/auth/r/ext/pay/deposits/unlinked", body=body) 191 | 192 | return [MerchantUnlinkedDeposit(**sub_data) for sub_data in data] 193 | -------------------------------------------------------------------------------- /bfxapi/rest/_interfaces/rest_public_endpoints.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | from typing import Any, Dict, List, Literal, Optional, Union, cast 3 | 4 | from bfxapi.rest._interface import Interface 5 | from bfxapi.types import ( 6 | Candle, 7 | DerivativesStatus, 8 | FundingCurrencyBook, 9 | FundingCurrencyRawBook, 10 | FundingCurrencyTicker, 11 | FundingCurrencyTrade, 12 | FundingMarketAveragePrice, 13 | FundingStatistic, 14 | FxRate, 15 | Leaderboard, 16 | Liquidation, 17 | PlatformStatus, 18 | PulseMessage, 19 | PulseProfile, 20 | Statistic, 21 | TickersHistory, 22 | TradingMarketAveragePrice, 23 | TradingPairBook, 24 | TradingPairRawBook, 25 | TradingPairTicker, 26 | TradingPairTrade, 27 | serializers, 28 | ) 29 | 30 | 31 | class RestPublicEndpoints(Interface): 32 | def conf(self, config: str) -> Any: 33 | return self._m.get(f"conf/{config}")[0] 34 | 35 | def get_platform_status(self) -> PlatformStatus: 36 | return serializers.PlatformStatus.parse(*self._m.get("platform/status")) 37 | 38 | def get_tickers( 39 | self, symbols: List[str] 40 | ) -> Dict[str, Union[TradingPairTicker, FundingCurrencyTicker]]: 41 | data = self._m.get("tickers", params={"symbols": ",".join(symbols)}) 42 | 43 | parsers = { 44 | "t": serializers.TradingPairTicker.parse, 45 | "f": serializers.FundingCurrencyTicker.parse, 46 | } 47 | 48 | return { 49 | symbol: cast( 50 | Union[TradingPairTicker, FundingCurrencyTicker], 51 | parsers[symbol[0]](*sub_data), 52 | ) 53 | for sub_data in data 54 | if (symbol := sub_data.pop(0)) 55 | } 56 | 57 | def get_t_tickers( 58 | self, symbols: Union[List[str], Literal["ALL"]] 59 | ) -> Dict[str, TradingPairTicker]: 60 | if isinstance(symbols, str) and symbols == "ALL": 61 | return { 62 | symbol: cast(TradingPairTicker, sub_data) 63 | for symbol, sub_data in self.get_tickers(["ALL"]).items() 64 | if symbol.startswith("t") 65 | } 66 | 67 | data = self.get_tickers(list(symbols)) 68 | 69 | return cast(Dict[str, TradingPairTicker], data) 70 | 71 | def get_f_tickers( 72 | self, symbols: Union[List[str], Literal["ALL"]] 73 | ) -> Dict[str, FundingCurrencyTicker]: 74 | if isinstance(symbols, str) and symbols == "ALL": 75 | return { 76 | symbol: cast(FundingCurrencyTicker, sub_data) 77 | for symbol, sub_data in self.get_tickers(["ALL"]).items() 78 | if symbol.startswith("f") 79 | } 80 | 81 | data = self.get_tickers(list(symbols)) 82 | 83 | return cast(Dict[str, FundingCurrencyTicker], data) 84 | 85 | def get_t_ticker(self, symbol: str) -> TradingPairTicker: 86 | return serializers.TradingPairTicker.parse(*self._m.get(f"ticker/{symbol}")) 87 | 88 | def get_f_ticker(self, symbol: str) -> FundingCurrencyTicker: 89 | return serializers.FundingCurrencyTicker.parse(*self._m.get(f"ticker/{symbol}")) 90 | 91 | def get_tickers_history( 92 | self, 93 | symbols: List[str], 94 | *, 95 | start: Optional[str] = None, 96 | end: Optional[str] = None, 97 | limit: Optional[int] = None, 98 | ) -> List[TickersHistory]: 99 | return [ 100 | serializers.TickersHistory.parse(*sub_data) 101 | for sub_data in self._m.get( 102 | "tickers/hist", 103 | params={ 104 | "symbols": ",".join(symbols), 105 | "start": start, 106 | "end": end, 107 | "limit": limit, 108 | }, 109 | ) 110 | ] 111 | 112 | def get_t_trades( 113 | self, 114 | pair: str, 115 | *, 116 | limit: Optional[int] = None, 117 | start: Optional[str] = None, 118 | end: Optional[str] = None, 119 | sort: Optional[int] = None, 120 | ) -> List[TradingPairTrade]: 121 | params = {"limit": limit, "start": start, "end": end, "sort": sort} 122 | data = self._m.get(f"trades/{pair}/hist", params=params) 123 | return [serializers.TradingPairTrade.parse(*sub_data) for sub_data in data] 124 | 125 | def get_f_trades( 126 | self, 127 | currency: str, 128 | *, 129 | limit: Optional[int] = None, 130 | start: Optional[str] = None, 131 | end: Optional[str] = None, 132 | sort: Optional[int] = None, 133 | ) -> List[FundingCurrencyTrade]: 134 | params = {"limit": limit, "start": start, "end": end, "sort": sort} 135 | data = self._m.get(f"trades/{currency}/hist", params=params) 136 | return [serializers.FundingCurrencyTrade.parse(*sub_data) for sub_data in data] 137 | 138 | def get_t_book( 139 | self, 140 | pair: str, 141 | precision: Literal["P0", "P1", "P2", "P3", "P4"], 142 | *, 143 | len: Optional[Literal[1, 25, 100]] = None, 144 | ) -> List[TradingPairBook]: 145 | return [ 146 | serializers.TradingPairBook.parse(*sub_data) 147 | for sub_data in self._m.get(f"book/{pair}/{precision}", params={"len": len}) 148 | ] 149 | 150 | def get_f_book( 151 | self, 152 | currency: str, 153 | precision: Literal["P0", "P1", "P2", "P3", "P4"], 154 | *, 155 | len: Optional[Literal[1, 25, 100]] = None, 156 | ) -> List[FundingCurrencyBook]: 157 | return [ 158 | serializers.FundingCurrencyBook.parse(*sub_data) 159 | for sub_data in self._m.get( 160 | f"book/{currency}/{precision}", params={"len": len} 161 | ) 162 | ] 163 | 164 | def get_t_raw_book( 165 | self, pair: str, *, len: Optional[Literal[1, 25, 100]] = None 166 | ) -> List[TradingPairRawBook]: 167 | return [ 168 | serializers.TradingPairRawBook.parse(*sub_data) 169 | for sub_data in self._m.get(f"book/{pair}/R0", params={"len": len}) 170 | ] 171 | 172 | def get_f_raw_book( 173 | self, currency: str, *, len: Optional[Literal[1, 25, 100]] = None 174 | ) -> List[FundingCurrencyRawBook]: 175 | return [ 176 | serializers.FundingCurrencyRawBook.parse(*sub_data) 177 | for sub_data in self._m.get(f"book/{currency}/R0", params={"len": len}) 178 | ] 179 | 180 | def get_stats_hist( 181 | self, 182 | resource: str, 183 | *, 184 | sort: Optional[int] = None, 185 | start: Optional[str] = None, 186 | end: Optional[str] = None, 187 | limit: Optional[int] = None, 188 | ) -> List[Statistic]: 189 | params = {"sort": sort, "start": start, "end": end, "limit": limit} 190 | data = self._m.get(f"stats1/{resource}/hist", params=params) 191 | return [serializers.Statistic.parse(*sub_data) for sub_data in data] 192 | 193 | def get_stats_last( 194 | self, 195 | resource: str, 196 | *, 197 | sort: Optional[int] = None, 198 | start: Optional[str] = None, 199 | end: Optional[str] = None, 200 | limit: Optional[int] = None, 201 | ) -> Statistic: 202 | params = {"sort": sort, "start": start, "end": end, "limit": limit} 203 | data = self._m.get(f"stats1/{resource}/last", params=params) 204 | return serializers.Statistic.parse(*data) 205 | 206 | def get_candles_hist( 207 | self, 208 | symbol: str, 209 | tf: str = "1m", 210 | *, 211 | sort: Optional[int] = None, 212 | start: Optional[str] = None, 213 | end: Optional[str] = None, 214 | limit: Optional[int] = None, 215 | ) -> List[Candle]: 216 | params = {"sort": sort, "start": start, "end": end, "limit": limit} 217 | data = self._m.get(f"candles/trade:{tf}:{symbol}/hist", params=params) 218 | return [serializers.Candle.parse(*sub_data) for sub_data in data] 219 | 220 | def get_candles_last( 221 | self, 222 | symbol: str, 223 | tf: str = "1m", 224 | *, 225 | sort: Optional[int] = None, 226 | start: Optional[str] = None, 227 | end: Optional[str] = None, 228 | limit: Optional[int] = None, 229 | ) -> Candle: 230 | params = {"sort": sort, "start": start, "end": end, "limit": limit} 231 | data = self._m.get(f"candles/trade:{tf}:{symbol}/last", params=params) 232 | return serializers.Candle.parse(*data) 233 | 234 | def get_derivatives_status( 235 | self, keys: Union[List[str], Literal["ALL"]] 236 | ) -> Dict[str, DerivativesStatus]: 237 | if keys == "ALL": 238 | params = {"keys": "ALL"} 239 | else: 240 | params = {"keys": ",".join(keys)} 241 | 242 | data = self._m.get("status/deriv", params=params) 243 | 244 | return { 245 | key: serializers.DerivativesStatus.parse(*sub_data) 246 | for sub_data in data 247 | if (key := sub_data.pop(0)) 248 | } 249 | 250 | def get_derivatives_status_history( 251 | self, 252 | key: str, 253 | *, 254 | sort: Optional[int] = None, 255 | start: Optional[str] = None, 256 | end: Optional[str] = None, 257 | limit: Optional[int] = None, 258 | ) -> List[DerivativesStatus]: 259 | params = {"sort": sort, "start": start, "end": end, "limit": limit} 260 | data = self._m.get(f"status/deriv/{key}/hist", params=params) 261 | return [serializers.DerivativesStatus.parse(*sub_data) for sub_data in data] 262 | 263 | def get_liquidations( 264 | self, 265 | *, 266 | sort: Optional[int] = None, 267 | start: Optional[str] = None, 268 | end: Optional[str] = None, 269 | limit: Optional[int] = None, 270 | ) -> List[Liquidation]: 271 | params = {"sort": sort, "start": start, "end": end, "limit": limit} 272 | data = self._m.get("liquidations/hist", params=params) 273 | return [serializers.Liquidation.parse(*sub_data[0]) for sub_data in data] 274 | 275 | def get_seed_candles( 276 | self, 277 | symbol: str, 278 | tf: str = "1m", 279 | *, 280 | sort: Optional[int] = None, 281 | start: Optional[str] = None, 282 | end: Optional[str] = None, 283 | limit: Optional[int] = None, 284 | ) -> List[Candle]: 285 | params = {"sort": sort, "start": start, "end": end, "limit": limit} 286 | data = self._m.get(f"candles/trade:{tf}:{symbol}/hist", params=params) 287 | return [serializers.Candle.parse(*sub_data) for sub_data in data] 288 | 289 | def get_leaderboards_hist( 290 | self, 291 | resource: str, 292 | *, 293 | sort: Optional[int] = None, 294 | start: Optional[str] = None, 295 | end: Optional[str] = None, 296 | limit: Optional[int] = None, 297 | ) -> List[Leaderboard]: 298 | params = {"sort": sort, "start": start, "end": end, "limit": limit} 299 | data = self._m.get(f"rankings/{resource}/hist", params=params) 300 | return [serializers.Leaderboard.parse(*sub_data) for sub_data in data] 301 | 302 | def get_leaderboards_last( 303 | self, 304 | resource: str, 305 | *, 306 | sort: Optional[int] = None, 307 | start: Optional[str] = None, 308 | end: Optional[str] = None, 309 | limit: Optional[int] = None, 310 | ) -> Leaderboard: 311 | params = {"sort": sort, "start": start, "end": end, "limit": limit} 312 | data = self._m.get(f"rankings/{resource}/last", params=params) 313 | return serializers.Leaderboard.parse(*data) 314 | 315 | def get_funding_stats( 316 | self, 317 | symbol: str, 318 | *, 319 | start: Optional[str] = None, 320 | end: Optional[str] = None, 321 | limit: Optional[int] = None, 322 | ) -> List[FundingStatistic]: 323 | params = {"start": start, "end": end, "limit": limit} 324 | data = self._m.get(f"funding/stats/{symbol}/hist", params=params) 325 | return [serializers.FundingStatistic.parse(*sub_data) for sub_data in data] 326 | 327 | def get_pulse_profile_details(self, nickname: str) -> PulseProfile: 328 | return serializers.PulseProfile.parse(*self._m.get(f"pulse/profile/{nickname}")) 329 | 330 | def get_pulse_message_history( 331 | self, *, end: Optional[str] = None, limit: Optional[int] = None 332 | ) -> List[PulseMessage]: 333 | messages = [] 334 | 335 | for sub_data in self._m.get("pulse/hist", params={"end": end, "limit": limit}): 336 | sub_data[18] = sub_data[18][0] 337 | message = serializers.PulseMessage.parse(*sub_data) 338 | messages.append(message) 339 | 340 | return messages 341 | 342 | def get_trading_market_average_price( 343 | self, 344 | symbol: str, 345 | amount: Union[str, float, Decimal], 346 | *, 347 | price_limit: Optional[Union[str, float, Decimal]] = None, 348 | ) -> TradingMarketAveragePrice: 349 | return serializers.TradingMarketAveragePrice.parse( 350 | *self._m.post( 351 | "calc/trade/avg", 352 | body={"symbol": symbol, "amount": amount, "price_limit": price_limit}, 353 | ) 354 | ) 355 | 356 | def get_funding_market_average_price( 357 | self, 358 | symbol: str, 359 | amount: Union[str, float, Decimal], 360 | period: int, 361 | *, 362 | rate_limit: Optional[Union[str, float, Decimal]] = None, 363 | ) -> FundingMarketAveragePrice: 364 | return serializers.FundingMarketAveragePrice.parse( 365 | *self._m.post( 366 | "calc/trade/avg", 367 | body={ 368 | "symbol": symbol, 369 | "amount": amount, 370 | "period": period, 371 | "rate_limit": rate_limit, 372 | }, 373 | ) 374 | ) 375 | 376 | def get_fx_rate(self, ccy1: str, ccy2: str) -> FxRate: 377 | return serializers.FxRate.parse( 378 | *self._m.post("calc/fx", body={"ccy1": ccy1, "ccy2": ccy2}) 379 | ) 380 | -------------------------------------------------------------------------------- /bfxapi/rest/exceptions.py: -------------------------------------------------------------------------------- 1 | from bfxapi.exceptions import BfxBaseException 2 | 3 | 4 | class RequestParameterError(BfxBaseException): 5 | pass 6 | 7 | 8 | class GenericError(BfxBaseException): 9 | pass 10 | -------------------------------------------------------------------------------- /bfxapi/types/__init__.py: -------------------------------------------------------------------------------- 1 | from .dataclasses import ( 2 | BalanceAvailable, 3 | BalanceInfo, 4 | BaseMarginInfo, 5 | Candle, 6 | CurrencyConversion, 7 | DepositAddress, 8 | DerivativePositionCollateral, 9 | DerivativePositionCollateralLimits, 10 | DerivativesStatus, 11 | FundingAutoRenew, 12 | FundingCredit, 13 | FundingCurrencyBook, 14 | FundingCurrencyRawBook, 15 | FundingCurrencyTicker, 16 | FundingCurrencyTrade, 17 | FundingInfo, 18 | FundingLoan, 19 | FundingMarketAveragePrice, 20 | FundingOffer, 21 | FundingStatistic, 22 | FundingTrade, 23 | FxRate, 24 | InvoicePage, 25 | InvoiceStats, 26 | InvoiceSubmission, 27 | Leaderboard, 28 | Ledger, 29 | LightningNetworkInvoice, 30 | Liquidation, 31 | LoginHistory, 32 | MerchantDeposit, 33 | MerchantUnlinkedDeposit, 34 | Movement, 35 | Order, 36 | OrderTrade, 37 | PlatformStatus, 38 | Position, 39 | PositionAudit, 40 | PositionClaim, 41 | PositionHistory, 42 | PositionIncrease, 43 | PositionIncreaseInfo, 44 | PositionSnapshot, 45 | PulseMessage, 46 | PulseProfile, 47 | Statistic, 48 | SymbolMarginInfo, 49 | TickersHistory, 50 | Trade, 51 | TradingMarketAveragePrice, 52 | TradingPairBook, 53 | TradingPairRawBook, 54 | TradingPairTicker, 55 | TradingPairTrade, 56 | Transfer, 57 | UserInfo, 58 | Wallet, 59 | Withdrawal, 60 | ) 61 | from .notification import Notification 62 | -------------------------------------------------------------------------------- /bfxapi/types/dataclasses.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Any, Dict, List, Literal, Optional 3 | 4 | from .labeler import _Type, compose, partial 5 | 6 | # region Dataclass definitions for types of public use 7 | 8 | 9 | @dataclass 10 | class PlatformStatus(_Type): 11 | status: int 12 | 13 | 14 | @dataclass 15 | class TradingPairTicker(_Type): 16 | bid: float 17 | bid_size: float 18 | ask: float 19 | ask_size: float 20 | daily_change: float 21 | daily_change_relative: float 22 | last_price: float 23 | volume: float 24 | high: float 25 | low: float 26 | 27 | 28 | @dataclass 29 | class FundingCurrencyTicker(_Type): 30 | frr: float 31 | bid: float 32 | bid_period: int 33 | bid_size: float 34 | ask: float 35 | ask_period: int 36 | ask_size: float 37 | daily_change: float 38 | daily_change_relative: float 39 | last_price: float 40 | volume: float 41 | high: float 42 | low: float 43 | frr_amount_available: float 44 | 45 | 46 | @dataclass 47 | class TickersHistory(_Type): 48 | symbol: str 49 | bid: float 50 | ask: float 51 | mts: int 52 | 53 | 54 | @dataclass 55 | class TradingPairTrade(_Type): 56 | id: int 57 | mts: int 58 | amount: float 59 | price: float 60 | 61 | 62 | @dataclass 63 | class FundingCurrencyTrade(_Type): 64 | id: int 65 | mts: int 66 | amount: float 67 | rate: float 68 | period: int 69 | 70 | 71 | @dataclass 72 | class TradingPairBook(_Type): 73 | price: float 74 | count: int 75 | amount: float 76 | 77 | 78 | @dataclass 79 | class FundingCurrencyBook(_Type): 80 | rate: float 81 | period: int 82 | count: int 83 | amount: float 84 | 85 | 86 | @dataclass 87 | class TradingPairRawBook(_Type): 88 | order_id: int 89 | price: float 90 | amount: float 91 | 92 | 93 | @dataclass 94 | class FundingCurrencyRawBook(_Type): 95 | offer_id: int 96 | period: int 97 | rate: float 98 | amount: float 99 | 100 | 101 | @dataclass 102 | class Statistic(_Type): 103 | mts: int 104 | value: float 105 | 106 | 107 | @dataclass 108 | class Candle(_Type): 109 | mts: int 110 | open: int 111 | close: int 112 | high: int 113 | low: int 114 | volume: float 115 | 116 | 117 | @dataclass 118 | class DerivativesStatus(_Type): 119 | mts: int 120 | deriv_price: float 121 | spot_price: float 122 | insurance_fund_balance: float 123 | next_funding_evt_mts: int 124 | next_funding_accrued: float 125 | next_funding_step: int 126 | current_funding: float 127 | mark_price: float 128 | open_interest: float 129 | clamp_min: float 130 | clamp_max: float 131 | 132 | 133 | @dataclass 134 | class Liquidation(_Type): 135 | pos_id: int 136 | mts: int 137 | symbol: str 138 | amount: float 139 | base_price: float 140 | is_match: int 141 | is_market_sold: int 142 | liquidation_price: float 143 | 144 | 145 | @dataclass 146 | class Leaderboard(_Type): 147 | mts: int 148 | username: str 149 | ranking: int 150 | value: float 151 | twitter_handle: Optional[str] 152 | 153 | 154 | @dataclass 155 | class FundingStatistic(_Type): 156 | mts: int 157 | frr: float 158 | avg_period: float 159 | funding_amount: float 160 | funding_amount_used: float 161 | funding_below_threshold: float 162 | 163 | 164 | @dataclass 165 | class PulseProfile(_Type): 166 | puid: str 167 | mts: int 168 | nickname: str 169 | picture: str 170 | text: str 171 | twitter_handle: str 172 | followers: int 173 | following: int 174 | tipping_status: int 175 | 176 | 177 | @dataclass 178 | class PulseMessage(_Type): 179 | pid: str 180 | mts: int 181 | puid: str 182 | title: str 183 | content: str 184 | is_pin: int 185 | is_public: int 186 | comments_disabled: int 187 | tags: List[str] 188 | attachments: List[str] 189 | meta: List[Dict[str, Any]] 190 | likes: int 191 | profile: PulseProfile 192 | comments: int 193 | 194 | 195 | @dataclass 196 | class TradingMarketAveragePrice(_Type): 197 | price_avg: float 198 | amount: float 199 | 200 | 201 | @dataclass 202 | class FundingMarketAveragePrice(_Type): 203 | rate_avg: float 204 | amount: float 205 | 206 | 207 | @dataclass 208 | class FxRate(_Type): 209 | current_rate: float 210 | 211 | 212 | # endregion 213 | 214 | # region Dataclass definitions for types of auth use 215 | 216 | 217 | @dataclass 218 | class UserInfo(_Type): 219 | id: int 220 | email: str 221 | username: str 222 | mts_account_create: int 223 | verified: int 224 | verification_level: int 225 | timezone: str 226 | locale: str 227 | company: str 228 | email_verified: int 229 | mts_master_account_create: int 230 | group_id: int 231 | master_account_id: int 232 | inherit_master_account_verification: int 233 | is_group_master: int 234 | group_withdraw_enabled: int 235 | ppt_enabled: int 236 | merchant_enabled: int 237 | competition_enabled: int 238 | two_factors_authentication_modes: List[str] 239 | is_securities_master: int 240 | securities_enabled: int 241 | allow_disable_ctxswitch: int 242 | time_last_login: int 243 | ctxtswitch_disabled: int 244 | comp_countries: List[str] 245 | compl_countries_resid: List[str] 246 | is_merchant_enterprise: int 247 | 248 | 249 | @dataclass 250 | class LoginHistory(_Type): 251 | id: int 252 | time: int 253 | ip: str 254 | extra_info: Dict[str, Any] 255 | 256 | 257 | @dataclass 258 | class BalanceAvailable(_Type): 259 | amount: float 260 | 261 | 262 | @dataclass 263 | class Order(_Type): 264 | id: int 265 | gid: int 266 | cid: int 267 | symbol: str 268 | mts_create: int 269 | mts_update: int 270 | amount: float 271 | amount_orig: float 272 | order_type: str 273 | type_prev: str 274 | mts_tif: int 275 | flags: int 276 | order_status: str 277 | price: float 278 | price_avg: float 279 | price_trailing: float 280 | price_aux_limit: float 281 | notify: int 282 | hidden: int 283 | placed_id: int 284 | routing: str 285 | meta: Dict[str, Any] 286 | 287 | 288 | @dataclass 289 | class Position(_Type): 290 | symbol: str 291 | status: str 292 | amount: float 293 | base_price: float 294 | margin_funding: float 295 | margin_funding_type: int 296 | pl: float 297 | pl_perc: float 298 | price_liq: float 299 | leverage: float 300 | position_id: int 301 | mts_create: int 302 | mts_update: int 303 | type: int 304 | collateral: float 305 | collateral_min: float 306 | meta: Dict[str, Any] 307 | 308 | 309 | @dataclass 310 | class Trade(_Type): 311 | id: int 312 | symbol: str 313 | mts_create: int 314 | order_id: int 315 | exec_amount: float 316 | exec_price: float 317 | order_type: str 318 | order_price: float 319 | maker: int 320 | fee: float 321 | fee_currency: str 322 | cid: int 323 | 324 | 325 | @dataclass() 326 | class FundingTrade(_Type): 327 | id: int 328 | currency: str 329 | mts_create: int 330 | offer_id: int 331 | amount: float 332 | rate: float 333 | period: int 334 | 335 | 336 | @dataclass 337 | class OrderTrade(_Type): 338 | id: int 339 | symbol: str 340 | mts_create: int 341 | order_id: int 342 | exec_amount: float 343 | exec_price: float 344 | maker: int 345 | fee: float 346 | fee_currency: str 347 | cid: int 348 | 349 | 350 | @dataclass 351 | class Ledger(_Type): 352 | id: int 353 | currency: str 354 | mts: int 355 | amount: float 356 | balance: float 357 | description: str 358 | 359 | 360 | @dataclass 361 | class FundingOffer(_Type): 362 | id: int 363 | symbol: str 364 | mts_create: int 365 | mts_update: int 366 | amount: float 367 | amount_orig: float 368 | offer_type: str 369 | flags: int 370 | offer_status: str 371 | rate: float 372 | period: int 373 | notify: int 374 | hidden: int 375 | renew: int 376 | 377 | 378 | @dataclass 379 | class FundingCredit(_Type): 380 | id: int 381 | symbol: str 382 | side: int 383 | mts_create: int 384 | mts_update: int 385 | amount: float 386 | flags: int 387 | status: str 388 | rate_type: str 389 | rate: float 390 | period: int 391 | mts_opening: int 392 | mts_last_payout: int 393 | notify: int 394 | hidden: int 395 | renew: int 396 | no_close: int 397 | position_pair: str 398 | 399 | 400 | @dataclass 401 | class FundingLoan(_Type): 402 | id: int 403 | symbol: str 404 | side: int 405 | mts_create: int 406 | mts_update: int 407 | amount: float 408 | flags: int 409 | status: str 410 | rate_type: str 411 | rate: float 412 | period: int 413 | mts_opening: int 414 | mts_last_payout: int 415 | notify: int 416 | hidden: int 417 | renew: int 418 | no_close: int 419 | 420 | 421 | @dataclass 422 | class FundingAutoRenew(_Type): 423 | currency: str 424 | period: int 425 | rate: float 426 | threshold: float 427 | 428 | 429 | @dataclass() 430 | class FundingInfo(_Type): 431 | symbol: str 432 | yield_loan: float 433 | yield_lend: float 434 | duration_loan: float 435 | duration_lend: float 436 | 437 | 438 | @dataclass 439 | class Wallet(_Type): 440 | wallet_type: str 441 | currency: str 442 | balance: float 443 | unsettled_interest: float 444 | available_balance: float 445 | last_change: str 446 | trade_details: Dict[str, Any] 447 | 448 | 449 | @dataclass 450 | class Transfer(_Type): 451 | mts: int 452 | wallet_from: str 453 | wallet_to: str 454 | currency: str 455 | currency_to: str 456 | amount: int 457 | 458 | 459 | @dataclass 460 | class Withdrawal(_Type): 461 | withdrawal_id: int 462 | method: str 463 | payment_id: str 464 | wallet: str 465 | amount: float 466 | withdrawal_fee: float 467 | 468 | 469 | @dataclass 470 | class DepositAddress(_Type): 471 | method: str 472 | currency_code: str 473 | address: str 474 | pool_address: str 475 | 476 | 477 | @dataclass 478 | class LightningNetworkInvoice(_Type): 479 | invoice_hash: str 480 | invoice: str 481 | amount: str 482 | 483 | 484 | @dataclass 485 | class Movement(_Type): 486 | id: str 487 | currency: str 488 | currency_name: str 489 | mts_start: int 490 | mts_update: int 491 | status: str 492 | amount: int 493 | fees: int 494 | destination_address: str 495 | transaction_id: str 496 | withdraw_transaction_note: str 497 | 498 | 499 | @dataclass 500 | class SymbolMarginInfo(_Type): 501 | symbol: str 502 | tradable_balance: float 503 | gross_balance: float 504 | buy: float 505 | sell: float 506 | 507 | 508 | @dataclass 509 | class BaseMarginInfo(_Type): 510 | user_pl: float 511 | user_swaps: float 512 | margin_balance: float 513 | margin_net: float 514 | margin_min: float 515 | 516 | 517 | @dataclass 518 | class PositionClaim(_Type): 519 | symbol: str 520 | position_status: str 521 | amount: float 522 | base_price: float 523 | margin_funding: float 524 | margin_funding_type: int 525 | position_id: int 526 | mts_create: int 527 | mts_update: int 528 | pos_type: int 529 | collateral: str 530 | min_collateral: str 531 | meta: Dict[str, Any] 532 | 533 | 534 | @dataclass 535 | class PositionIncreaseInfo(_Type): 536 | max_pos: int 537 | current_pos: float 538 | base_currency_balance: float 539 | tradable_balance_quote_currency: float 540 | tradable_balance_quote_total: float 541 | tradable_balance_base_currency: float 542 | tradable_balance_base_total: float 543 | funding_avail: float 544 | funding_value: float 545 | funding_required: float 546 | funding_value_currency: str 547 | funding_required_currency: str 548 | 549 | 550 | @dataclass 551 | class PositionIncrease(_Type): 552 | symbol: str 553 | amount: float 554 | base_price: float 555 | 556 | 557 | @dataclass 558 | class PositionHistory(_Type): 559 | symbol: str 560 | status: str 561 | amount: float 562 | base_price: float 563 | funding: float 564 | funding_type: int 565 | position_id: int 566 | mts_create: int 567 | mts_update: int 568 | 569 | 570 | @dataclass 571 | class PositionSnapshot(_Type): 572 | symbol: str 573 | status: str 574 | amount: float 575 | base_price: float 576 | funding: float 577 | funding_type: int 578 | position_id: int 579 | mts_create: int 580 | mts_update: int 581 | 582 | 583 | @dataclass 584 | class PositionAudit(_Type): 585 | symbol: str 586 | status: str 587 | amount: float 588 | base_price: float 589 | funding: float 590 | funding_type: int 591 | position_id: int 592 | mts_create: int 593 | mts_update: int 594 | type: int 595 | collateral: float 596 | collateral_min: float 597 | meta: Dict[str, Any] 598 | 599 | 600 | @dataclass 601 | class DerivativePositionCollateral(_Type): 602 | status: int 603 | 604 | 605 | @dataclass 606 | class DerivativePositionCollateralLimits(_Type): 607 | min_collateral: float 608 | max_collateral: float 609 | 610 | 611 | @dataclass 612 | class BalanceInfo(_Type): 613 | aum: float 614 | aum_net: float 615 | 616 | 617 | # endregion 618 | 619 | # region Dataclass definitions for types of merchant use 620 | 621 | 622 | @compose(dataclass, partial) 623 | class InvoiceSubmission(_Type): 624 | id: str 625 | t: int 626 | type: Literal["ECOMMERCE", "POS"] 627 | duration: int 628 | amount: float 629 | currency: str 630 | order_id: str 631 | pay_currencies: List[str] 632 | webhook: str 633 | redirect_url: str 634 | status: Literal["CREATED", "PENDING", "COMPLETED", "EXPIRED"] 635 | customer_info: "CustomerInfo" 636 | invoices: List["Invoice"] 637 | payment: "Payment" 638 | additional_payments: List["Payment"] 639 | merchant_name: str 640 | 641 | @classmethod 642 | def parse(cls, data: Dict[str, Any]) -> "InvoiceSubmission": 643 | if "customer_info" in data and data["customer_info"] is not None: 644 | data["customer_info"] = InvoiceSubmission.CustomerInfo( 645 | **data["customer_info"] 646 | ) 647 | 648 | for index, invoice in enumerate(data["invoices"]): 649 | data["invoices"][index] = InvoiceSubmission.Invoice(**invoice) 650 | 651 | if "payment" in data and data["payment"] is not None: 652 | data["payment"] = InvoiceSubmission.Payment(**data["payment"]) 653 | 654 | if "additional_payments" in data and data["additional_payments"] is not None: 655 | for index, additional_payment in enumerate(data["additional_payments"]): 656 | data["additional_payments"][index] = InvoiceSubmission.Payment( 657 | **additional_payment 658 | ) 659 | 660 | return InvoiceSubmission(**data) 661 | 662 | @compose(dataclass, partial) 663 | class CustomerInfo: 664 | nationality: str 665 | resid_country: str 666 | resid_state: str 667 | resid_city: str 668 | resid_zip_code: str 669 | resid_street: str 670 | resid_building_no: str 671 | full_name: str 672 | email: str 673 | tos_accepted: bool 674 | 675 | @compose(dataclass, partial) 676 | class Invoice: 677 | amount: float 678 | currency: str 679 | pay_currency: str 680 | pool_currency: str 681 | address: str 682 | ext: Dict[str, Any] 683 | 684 | @compose(dataclass, partial) 685 | class Payment: 686 | txid: str 687 | amount: float 688 | currency: str 689 | method: str 690 | status: Literal["CREATED", "COMPLETED", "PROCESSING"] 691 | confirmations: int 692 | created_at: str 693 | updated_at: str 694 | deposit_id: int 695 | ledger_id: int 696 | force_completed: bool 697 | amount_diff: str 698 | 699 | 700 | @dataclass 701 | class InvoicePage(_Type): 702 | page: int 703 | page_size: int 704 | sort: Literal["asc", "desc"] 705 | sort_field: Literal["t", "amount", "status"] 706 | total_pages: int 707 | total_items: int 708 | items: List[InvoiceSubmission] 709 | 710 | @classmethod 711 | def parse(cls, data: Dict[str, Any]) -> "InvoicePage": 712 | for index, item in enumerate(data["items"]): 713 | data["items"][index] = InvoiceSubmission.parse(item) 714 | 715 | return InvoicePage(**data) 716 | 717 | 718 | @dataclass 719 | class InvoiceStats(_Type): 720 | time: str 721 | count: float 722 | 723 | 724 | @dataclass 725 | class CurrencyConversion(_Type): 726 | base_ccy: str 727 | convert_ccy: str 728 | created: int 729 | 730 | 731 | @dataclass 732 | class MerchantDeposit(_Type): 733 | id: int 734 | invoice_id: Optional[str] 735 | order_id: Optional[str] 736 | type: Literal["ledger", "deposit"] 737 | amount: float 738 | t: int 739 | txid: str 740 | currency: str 741 | method: str 742 | pay_method: str 743 | 744 | 745 | @dataclass 746 | class MerchantUnlinkedDeposit(_Type): 747 | id: int 748 | method: str 749 | currency: str 750 | created_at: int 751 | updated_at: int 752 | amount: float 753 | fee: float 754 | txid: str 755 | address: str 756 | payment_id: Optional[int] 757 | status: str 758 | note: Optional[str] 759 | 760 | 761 | # endregion 762 | -------------------------------------------------------------------------------- /bfxapi/types/labeler.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, Generic, Iterable, List, Tuple, Type, TypeVar, cast 2 | 3 | T = TypeVar("T", bound="_Type") 4 | 5 | 6 | def compose(*decorators): 7 | def wrapper(function): 8 | for decorator in reversed(decorators): 9 | function = decorator(function) 10 | return function 11 | 12 | return wrapper 13 | 14 | 15 | def partial(cls): 16 | def __init__(self, **kwargs): 17 | for annotation in self.__annotations__.keys(): 18 | if annotation not in kwargs: 19 | self.__setattr__(annotation, None) 20 | else: 21 | self.__setattr__(annotation, kwargs[annotation]) 22 | 23 | kwargs.pop(annotation, None) 24 | 25 | if len(kwargs) != 0: 26 | raise TypeError( 27 | f"{cls.__name__}.__init__() got an unexpected keyword argument " 28 | f"'{list(kwargs.keys())[0]}'" 29 | ) 30 | 31 | cls.__init__ = __init__ 32 | 33 | return cls 34 | 35 | 36 | class _Type: 37 | """ 38 | Base class for any dataclass serializable by the _Serializer generic class. 39 | """ 40 | 41 | 42 | class _Serializer(Generic[T]): 43 | def __init__( 44 | self, name: str, klass: Type[_Type], labels: List[str], *, flat: bool = False 45 | ): 46 | self.name, self.klass, self.__labels, self.__flat = name, klass, labels, flat 47 | 48 | def _serialize(self, *args: Any) -> Iterable[Tuple[str, Any]]: 49 | if self.__flat: 50 | args = tuple(_Serializer.__flatten(list(args))) 51 | 52 | if len(self.__labels) > len(args): 53 | raise AssertionError( 54 | f"{self.name} -> and <*args> " 55 | "arguments should contain the same amount of elements." 56 | ) 57 | 58 | for index, label in enumerate(self.__labels): 59 | if label != "_PLACEHOLDER": 60 | yield label, args[index] 61 | 62 | def parse(self, *values: Any) -> T: 63 | return cast(T, self.klass(**dict(self._serialize(*values)))) 64 | 65 | def get_labels(self) -> List[str]: 66 | return [label for label in self.__labels if label != "_PLACEHOLDER"] 67 | 68 | @classmethod 69 | def __flatten(cls, array: List[Any]) -> List[Any]: 70 | if len(array) == 0: 71 | return array 72 | 73 | if isinstance(array[0], list): 74 | return cls.__flatten(array[0]) + cls.__flatten(array[1:]) 75 | 76 | return array[:1] + cls.__flatten(array[1:]) 77 | 78 | 79 | class _RecursiveSerializer(_Serializer, Generic[T]): 80 | def __init__( 81 | self, 82 | name: str, 83 | klass: Type[_Type], 84 | labels: List[str], 85 | *, 86 | serializers: Dict[str, _Serializer[Any]], 87 | flat: bool = False, 88 | ): 89 | super().__init__(name, klass, labels, flat=flat) 90 | 91 | self.serializers = serializers 92 | 93 | def parse(self, *values: Any) -> T: 94 | serialization = dict(self._serialize(*values)) 95 | 96 | for key in serialization: 97 | if key in self.serializers.keys(): 98 | serialization[key] = self.serializers[key].parse(*serialization[key]) 99 | 100 | return cast(T, self.klass(**serialization)) 101 | 102 | 103 | def generate_labeler_serializer( 104 | name: str, klass: Type[T], labels: List[str], *, flat: bool = False 105 | ) -> _Serializer[T]: 106 | return _Serializer[T](name, klass, labels, flat=flat) 107 | 108 | 109 | def generate_recursive_serializer( 110 | name: str, 111 | klass: Type[T], 112 | labels: List[str], 113 | *, 114 | serializers: Dict[str, _Serializer[Any]], 115 | flat: bool = False, 116 | ) -> _RecursiveSerializer[T]: 117 | return _RecursiveSerializer[T]( 118 | name, klass, labels, serializers=serializers, flat=flat 119 | ) 120 | -------------------------------------------------------------------------------- /bfxapi/types/notification.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Any, Generic, List, Optional, TypeVar, cast 3 | 4 | from .labeler import _Serializer, _Type 5 | 6 | T = TypeVar("T") 7 | 8 | 9 | @dataclass 10 | class Notification(_Type, Generic[T]): 11 | mts: int 12 | type: str 13 | message_id: Optional[int] 14 | data: T 15 | code: Optional[int] 16 | status: str 17 | text: str 18 | 19 | 20 | class _Notification(_Serializer, Generic[T]): 21 | __LABELS = [ 22 | "mts", 23 | "type", 24 | "message_id", 25 | "_PLACEHOLDER", 26 | "data", 27 | "code", 28 | "status", 29 | "text", 30 | ] 31 | 32 | def __init__( 33 | self, serializer: Optional[_Serializer] = None, is_iterable: bool = False 34 | ): 35 | super().__init__("Notification", Notification, _Notification.__LABELS) 36 | 37 | self.serializer, self.is_iterable = serializer, is_iterable 38 | 39 | def parse(self, *values: Any) -> Notification[T]: 40 | notification = cast( 41 | Notification[T], Notification(**dict(self._serialize(*values))) 42 | ) 43 | 44 | if isinstance(self.serializer, _Serializer): 45 | data = cast(List[Any], notification.data) 46 | 47 | if not self.is_iterable: 48 | if len(data) == 1 and isinstance(data[0], list): 49 | data = data[0] 50 | 51 | notification.data = self.serializer.parse(*data) 52 | else: 53 | notification.data = cast( 54 | T, [self.serializer.parse(*sub_data) for sub_data in data] 55 | ) 56 | 57 | return notification 58 | -------------------------------------------------------------------------------- /bfxapi/types/serializers.py: -------------------------------------------------------------------------------- 1 | from . import dataclasses 2 | from .labeler import ( # noqa: F401 3 | _Serializer, 4 | generate_labeler_serializer, 5 | generate_recursive_serializer, 6 | ) 7 | from .notification import _Notification # noqa: F401 8 | 9 | __serializers__ = [ 10 | "PlatformStatus", 11 | "TradingPairTicker", 12 | "FundingCurrencyTicker", 13 | "TickersHistory", 14 | "TradingPairTrade", 15 | "FundingCurrencyTrade", 16 | "TradingPairBook", 17 | "FundingCurrencyBook", 18 | "TradingPairRawBook", 19 | "FundingCurrencyRawBook", 20 | "Statistic", 21 | "Candle", 22 | "DerivativesStatus", 23 | "Liquidation", 24 | "Leaderboard", 25 | "FundingStatistic", 26 | "PulseProfile", 27 | "PulseMessage", 28 | "TradingMarketAveragePrice", 29 | "FundingMarketAveragePrice", 30 | "FxRate", 31 | "UserInfo", 32 | "LoginHistory", 33 | "BalanceAvailable", 34 | "Order", 35 | "Position", 36 | "Trade", 37 | "FundingTrade", 38 | "OrderTrade", 39 | "Ledger", 40 | "FundingOffer", 41 | "FundingCredit", 42 | "FundingLoan", 43 | "FundingAutoRenew", 44 | "FundingInfo", 45 | "Wallet", 46 | "Transfer", 47 | "Withdrawal", 48 | "DepositAddress", 49 | "LightningNetworkInvoice", 50 | "Movement", 51 | "SymbolMarginInfo", 52 | "BaseMarginInfo", 53 | "PositionClaim", 54 | "PositionIncreaseInfo", 55 | "PositionIncrease", 56 | "PositionHistory", 57 | "PositionSnapshot", 58 | "PositionAudit", 59 | "DerivativePositionCollateral", 60 | "DerivativePositionCollateralLimits", 61 | ] 62 | 63 | # region Serializer definitions for types of public use 64 | 65 | PlatformStatus = generate_labeler_serializer( 66 | name="PlatformStatus", klass=dataclasses.PlatformStatus, labels=["status"] 67 | ) 68 | 69 | TradingPairTicker = generate_labeler_serializer( 70 | name="TradingPairTicker", 71 | klass=dataclasses.TradingPairTicker, 72 | labels=[ 73 | "bid", 74 | "bid_size", 75 | "ask", 76 | "ask_size", 77 | "daily_change", 78 | "daily_change_relative", 79 | "last_price", 80 | "volume", 81 | "high", 82 | "low", 83 | ], 84 | ) 85 | 86 | FundingCurrencyTicker = generate_labeler_serializer( 87 | name="FundingCurrencyTicker", 88 | klass=dataclasses.FundingCurrencyTicker, 89 | labels=[ 90 | "frr", 91 | "bid", 92 | "bid_period", 93 | "bid_size", 94 | "ask", 95 | "ask_period", 96 | "ask_size", 97 | "daily_change", 98 | "daily_change_relative", 99 | "last_price", 100 | "volume", 101 | "high", 102 | "low", 103 | "_PLACEHOLDER", 104 | "_PLACEHOLDER", 105 | "frr_amount_available", 106 | ], 107 | ) 108 | 109 | TickersHistory = generate_labeler_serializer( 110 | name="TickersHistory", 111 | klass=dataclasses.TickersHistory, 112 | labels=[ 113 | "symbol", 114 | "bid", 115 | "_PLACEHOLDER", 116 | "ask", 117 | "_PLACEHOLDER", 118 | "_PLACEHOLDER", 119 | "_PLACEHOLDER", 120 | "_PLACEHOLDER", 121 | "_PLACEHOLDER", 122 | "_PLACEHOLDER", 123 | "_PLACEHOLDER", 124 | "_PLACEHOLDER", 125 | "mts", 126 | ], 127 | ) 128 | 129 | TradingPairTrade = generate_labeler_serializer( 130 | name="TradingPairTrade", 131 | klass=dataclasses.TradingPairTrade, 132 | labels=["id", "mts", "amount", "price"], 133 | ) 134 | 135 | FundingCurrencyTrade = generate_labeler_serializer( 136 | name="FundingCurrencyTrade", 137 | klass=dataclasses.FundingCurrencyTrade, 138 | labels=["id", "mts", "amount", "rate", "period"], 139 | ) 140 | 141 | TradingPairBook = generate_labeler_serializer( 142 | name="TradingPairBook", 143 | klass=dataclasses.TradingPairBook, 144 | labels=["price", "count", "amount"], 145 | ) 146 | 147 | FundingCurrencyBook = generate_labeler_serializer( 148 | name="FundingCurrencyBook", 149 | klass=dataclasses.FundingCurrencyBook, 150 | labels=["rate", "period", "count", "amount"], 151 | ) 152 | 153 | TradingPairRawBook = generate_labeler_serializer( 154 | name="TradingPairRawBook", 155 | klass=dataclasses.TradingPairRawBook, 156 | labels=["order_id", "price", "amount"], 157 | ) 158 | 159 | FundingCurrencyRawBook = generate_labeler_serializer( 160 | name="FundingCurrencyRawBook", 161 | klass=dataclasses.FundingCurrencyRawBook, 162 | labels=["offer_id", "period", "rate", "amount"], 163 | ) 164 | 165 | Statistic = generate_labeler_serializer( 166 | name="Statistic", klass=dataclasses.Statistic, labels=["mts", "value"] 167 | ) 168 | 169 | Candle = generate_labeler_serializer( 170 | name="Candle", 171 | klass=dataclasses.Candle, 172 | labels=["mts", "open", "close", "high", "low", "volume"], 173 | ) 174 | 175 | DerivativesStatus = generate_labeler_serializer( 176 | name="DerivativesStatus", 177 | klass=dataclasses.DerivativesStatus, 178 | labels=[ 179 | "mts", 180 | "_PLACEHOLDER", 181 | "deriv_price", 182 | "spot_price", 183 | "_PLACEHOLDER", 184 | "insurance_fund_balance", 185 | "_PLACEHOLDER", 186 | "next_funding_evt_mts", 187 | "next_funding_accrued", 188 | "next_funding_step", 189 | "_PLACEHOLDER", 190 | "current_funding", 191 | "_PLACEHOLDER", 192 | "_PLACEHOLDER", 193 | "mark_price", 194 | "_PLACEHOLDER", 195 | "_PLACEHOLDER", 196 | "open_interest", 197 | "_PLACEHOLDER", 198 | "_PLACEHOLDER", 199 | "_PLACEHOLDER", 200 | "clamp_min", 201 | "clamp_max", 202 | ], 203 | ) 204 | 205 | Liquidation = generate_labeler_serializer( 206 | name="Liquidation", 207 | klass=dataclasses.Liquidation, 208 | labels=[ 209 | "_PLACEHOLDER", 210 | "pos_id", 211 | "mts", 212 | "_PLACEHOLDER", 213 | "symbol", 214 | "amount", 215 | "base_price", 216 | "_PLACEHOLDER", 217 | "is_match", 218 | "is_market_sold", 219 | "_PLACEHOLDER", 220 | "liquidation_price", 221 | ], 222 | ) 223 | 224 | Leaderboard = generate_labeler_serializer( 225 | name="Leaderboard", 226 | klass=dataclasses.Leaderboard, 227 | labels=[ 228 | "mts", 229 | "_PLACEHOLDER", 230 | "username", 231 | "ranking", 232 | "_PLACEHOLDER", 233 | "_PLACEHOLDER", 234 | "value", 235 | "_PLACEHOLDER", 236 | "_PLACEHOLDER", 237 | "twitter_handle", 238 | ], 239 | ) 240 | 241 | FundingStatistic = generate_labeler_serializer( 242 | name="FundingStatistic", 243 | klass=dataclasses.FundingStatistic, 244 | labels=[ 245 | "mts", 246 | "_PLACEHOLDER", 247 | "_PLACEHOLDER", 248 | "frr", 249 | "avg_period", 250 | "_PLACEHOLDER", 251 | "_PLACEHOLDER", 252 | "funding_amount", 253 | "funding_amount_used", 254 | "_PLACEHOLDER", 255 | "_PLACEHOLDER", 256 | "funding_below_threshold", 257 | ], 258 | ) 259 | 260 | PulseProfile = generate_labeler_serializer( 261 | name="PulseProfile", 262 | klass=dataclasses.PulseProfile, 263 | labels=[ 264 | "puid", 265 | "mts", 266 | "_PLACEHOLDER", 267 | "nickname", 268 | "_PLACEHOLDER", 269 | "picture", 270 | "text", 271 | "_PLACEHOLDER", 272 | "_PLACEHOLDER", 273 | "twitter_handle", 274 | "_PLACEHOLDER", 275 | "followers", 276 | "following", 277 | "_PLACEHOLDER", 278 | "_PLACEHOLDER", 279 | "_PLACEHOLDER", 280 | "tipping_status", 281 | ], 282 | ) 283 | 284 | PulseMessage = generate_recursive_serializer( 285 | name="PulseMessage", 286 | klass=dataclasses.PulseMessage, 287 | serializers={"profile": PulseProfile}, 288 | labels=[ 289 | "pid", 290 | "mts", 291 | "_PLACEHOLDER", 292 | "puid", 293 | "_PLACEHOLDER", 294 | "title", 295 | "content", 296 | "_PLACEHOLDER", 297 | "_PLACEHOLDER", 298 | "is_pin", 299 | "is_public", 300 | "comments_disabled", 301 | "tags", 302 | "attachments", 303 | "meta", 304 | "likes", 305 | "_PLACEHOLDER", 306 | "_PLACEHOLDER", 307 | "profile", 308 | "comments", 309 | "_PLACEHOLDER", 310 | "_PLACEHOLDER", 311 | ], 312 | ) 313 | 314 | TradingMarketAveragePrice = generate_labeler_serializer( 315 | name="TradingMarketAveragePrice", 316 | klass=dataclasses.TradingMarketAveragePrice, 317 | labels=["price_avg", "amount"], 318 | ) 319 | 320 | FundingMarketAveragePrice = generate_labeler_serializer( 321 | name="FundingMarketAveragePrice", 322 | klass=dataclasses.FundingMarketAveragePrice, 323 | labels=["rate_avg", "amount"], 324 | ) 325 | 326 | FxRate = generate_labeler_serializer( 327 | name="FxRate", klass=dataclasses.FxRate, labels=["current_rate"] 328 | ) 329 | 330 | # endregion 331 | 332 | # region Serializer definitions for types of auth use 333 | 334 | UserInfo = generate_labeler_serializer( 335 | name="UserInfo", 336 | klass=dataclasses.UserInfo, 337 | labels=[ 338 | "id", 339 | "email", 340 | "username", 341 | "mts_account_create", 342 | "verified", 343 | "verification_level", 344 | "_PLACEHOLDER", 345 | "timezone", 346 | "locale", 347 | "company", 348 | "email_verified", 349 | "_PLACEHOLDER", 350 | "_PLACEHOLDER", 351 | "_PLACEHOLDER", 352 | "mts_master_account_create", 353 | "group_id", 354 | "master_account_id", 355 | "inherit_master_account_verification", 356 | "is_group_master", 357 | "group_withdraw_enabled", 358 | "_PLACEHOLDER", 359 | "_PLACEHOLDER", 360 | "_PLACEHOLDER", 361 | "ppt_enabled", 362 | "merchant_enabled", 363 | "competition_enabled", 364 | "two_factors_authentication_modes", 365 | "_PLACEHOLDER", 366 | "_PLACEHOLDER", 367 | "_PLACEHOLDER", 368 | "_PLACEHOLDER", 369 | "_PLACEHOLDER", 370 | "_PLACEHOLDER", 371 | "_PLACEHOLDER", 372 | "is_securities_master", 373 | "_PLACEHOLDER", 374 | "_PLACEHOLDER", 375 | "_PLACEHOLDER", 376 | "securities_enabled", 377 | "allow_disable_ctxswitch", 378 | "_PLACEHOLDER", 379 | "_PLACEHOLDER", 380 | "_PLACEHOLDER", 381 | "_PLACEHOLDER", 382 | "time_last_login", 383 | "_PLACEHOLDER", 384 | "_PLACEHOLDER", 385 | "ctxtswitch_disabled", 386 | "_PLACEHOLDER", 387 | "comp_countries", 388 | "compl_countries_resid", 389 | "_PLACEHOLDER", 390 | "_PLACEHOLDER", 391 | "_PLACEHOLDER", 392 | "is_merchant_enterprise", 393 | ], 394 | ) 395 | 396 | LoginHistory = generate_labeler_serializer( 397 | name="LoginHistory", 398 | klass=dataclasses.LoginHistory, 399 | labels=[ 400 | "id", 401 | "_PLACEHOLDER", 402 | "time", 403 | "_PLACEHOLDER", 404 | "ip", 405 | "_PLACEHOLDER", 406 | "_PLACEHOLDER", 407 | "extra_info", 408 | ], 409 | ) 410 | 411 | BalanceAvailable = generate_labeler_serializer( 412 | name="BalanceAvailable", klass=dataclasses.BalanceAvailable, labels=["amount"] 413 | ) 414 | 415 | Order = generate_labeler_serializer( 416 | name="Order", 417 | klass=dataclasses.Order, 418 | labels=[ 419 | "id", 420 | "gid", 421 | "cid", 422 | "symbol", 423 | "mts_create", 424 | "mts_update", 425 | "amount", 426 | "amount_orig", 427 | "order_type", 428 | "type_prev", 429 | "mts_tif", 430 | "_PLACEHOLDER", 431 | "flags", 432 | "order_status", 433 | "_PLACEHOLDER", 434 | "_PLACEHOLDER", 435 | "price", 436 | "price_avg", 437 | "price_trailing", 438 | "price_aux_limit", 439 | "_PLACEHOLDER", 440 | "_PLACEHOLDER", 441 | "_PLACEHOLDER", 442 | "notify", 443 | "hidden", 444 | "placed_id", 445 | "_PLACEHOLDER", 446 | "_PLACEHOLDER", 447 | "routing", 448 | "_PLACEHOLDER", 449 | "_PLACEHOLDER", 450 | "meta", 451 | ], 452 | ) 453 | 454 | Position = generate_labeler_serializer( 455 | name="Position", 456 | klass=dataclasses.Position, 457 | labels=[ 458 | "symbol", 459 | "status", 460 | "amount", 461 | "base_price", 462 | "margin_funding", 463 | "margin_funding_type", 464 | "pl", 465 | "pl_perc", 466 | "price_liq", 467 | "leverage", 468 | "_PLACEHOLDER", 469 | "position_id", 470 | "mts_create", 471 | "mts_update", 472 | "_PLACEHOLDER", 473 | "type", 474 | "_PLACEHOLDER", 475 | "collateral", 476 | "collateral_min", 477 | "meta", 478 | ], 479 | ) 480 | 481 | Trade = generate_labeler_serializer( 482 | name="Trade", 483 | klass=dataclasses.Trade, 484 | labels=[ 485 | "id", 486 | "symbol", 487 | "mts_create", 488 | "order_id", 489 | "exec_amount", 490 | "exec_price", 491 | "order_type", 492 | "order_price", 493 | "maker", 494 | "fee", 495 | "fee_currency", 496 | "cid", 497 | ], 498 | ) 499 | 500 | FundingTrade = generate_labeler_serializer( 501 | name="FundingTrade", 502 | klass=dataclasses.FundingTrade, 503 | labels=["id", "currency", "mts_create", "offer_id", "amount", "rate", "period"], 504 | ) 505 | 506 | OrderTrade = generate_labeler_serializer( 507 | name="OrderTrade", 508 | klass=dataclasses.OrderTrade, 509 | labels=[ 510 | "id", 511 | "symbol", 512 | "mts_create", 513 | "order_id", 514 | "exec_amount", 515 | "exec_price", 516 | "_PLACEHOLDER", 517 | "_PLACEHOLDER", 518 | "maker", 519 | "fee", 520 | "fee_currency", 521 | "cid", 522 | ], 523 | ) 524 | 525 | Ledger = generate_labeler_serializer( 526 | name="Ledger", 527 | klass=dataclasses.Ledger, 528 | labels=[ 529 | "id", 530 | "currency", 531 | "_PLACEHOLDER", 532 | "mts", 533 | "_PLACEHOLDER", 534 | "amount", 535 | "balance", 536 | "_PLACEHOLDER", 537 | "description", 538 | ], 539 | ) 540 | 541 | FundingOffer = generate_labeler_serializer( 542 | name="FundingOffer", 543 | klass=dataclasses.FundingOffer, 544 | labels=[ 545 | "id", 546 | "symbol", 547 | "mts_create", 548 | "mts_update", 549 | "amount", 550 | "amount_orig", 551 | "offer_type", 552 | "_PLACEHOLDER", 553 | "_PLACEHOLDER", 554 | "flags", 555 | "offer_status", 556 | "_PLACEHOLDER", 557 | "_PLACEHOLDER", 558 | "_PLACEHOLDER", 559 | "rate", 560 | "period", 561 | "notify", 562 | "hidden", 563 | "_PLACEHOLDER", 564 | "renew", 565 | "_PLACEHOLDER", 566 | ], 567 | ) 568 | 569 | FundingCredit = generate_labeler_serializer( 570 | name="FundingCredit", 571 | klass=dataclasses.FundingCredit, 572 | labels=[ 573 | "id", 574 | "symbol", 575 | "side", 576 | "mts_create", 577 | "mts_update", 578 | "amount", 579 | "flags", 580 | "status", 581 | "rate_type", 582 | "_PLACEHOLDER", 583 | "_PLACEHOLDER", 584 | "rate", 585 | "period", 586 | "mts_opening", 587 | "mts_last_payout", 588 | "notify", 589 | "hidden", 590 | "_PLACEHOLDER", 591 | "renew", 592 | "_PLACEHOLDER", 593 | "no_close", 594 | "position_pair", 595 | ], 596 | ) 597 | 598 | FundingLoan = generate_labeler_serializer( 599 | name="FundingLoan", 600 | klass=dataclasses.FundingLoan, 601 | labels=[ 602 | "id", 603 | "symbol", 604 | "side", 605 | "mts_create", 606 | "mts_update", 607 | "amount", 608 | "flags", 609 | "status", 610 | "rate_type", 611 | "_PLACEHOLDER", 612 | "_PLACEHOLDER", 613 | "rate", 614 | "period", 615 | "mts_opening", 616 | "mts_last_payout", 617 | "notify", 618 | "hidden", 619 | "_PLACEHOLDER", 620 | "renew", 621 | "_PLACEHOLDER", 622 | "no_close", 623 | ], 624 | ) 625 | 626 | FundingAutoRenew = generate_labeler_serializer( 627 | name="FundingAutoRenew", 628 | klass=dataclasses.FundingAutoRenew, 629 | labels=["currency", "period", "rate", "threshold"], 630 | ) 631 | 632 | FundingInfo = generate_labeler_serializer( 633 | name="FundingInfo", 634 | klass=dataclasses.FundingInfo, 635 | labels=[ 636 | "_PLACEHOLDER", 637 | "symbol", 638 | "yield_loan", 639 | "yield_lend", 640 | "duration_loan", 641 | "duration_lend", 642 | ], 643 | flat=True, 644 | ) 645 | 646 | Wallet = generate_labeler_serializer( 647 | name="Wallet", 648 | klass=dataclasses.Wallet, 649 | labels=[ 650 | "wallet_type", 651 | "currency", 652 | "balance", 653 | "unsettled_interest", 654 | "available_balance", 655 | "last_change", 656 | "trade_details", 657 | ], 658 | ) 659 | 660 | Transfer = generate_labeler_serializer( 661 | name="Transfer", 662 | klass=dataclasses.Transfer, 663 | labels=[ 664 | "mts", 665 | "wallet_from", 666 | "wallet_to", 667 | "_PLACEHOLDER", 668 | "currency", 669 | "currency_to", 670 | "_PLACEHOLDER", 671 | "amount", 672 | ], 673 | ) 674 | 675 | Withdrawal = generate_labeler_serializer( 676 | name="Withdrawal", 677 | klass=dataclasses.Withdrawal, 678 | labels=[ 679 | "withdrawal_id", 680 | "_PLACEHOLDER", 681 | "method", 682 | "payment_id", 683 | "wallet", 684 | "amount", 685 | "_PLACEHOLDER", 686 | "_PLACEHOLDER", 687 | "withdrawal_fee", 688 | ], 689 | ) 690 | 691 | DepositAddress = generate_labeler_serializer( 692 | name="DepositAddress", 693 | klass=dataclasses.DepositAddress, 694 | labels=[ 695 | "_PLACEHOLDER", 696 | "method", 697 | "currency_code", 698 | "_PLACEHOLDER", 699 | "address", 700 | "pool_address", 701 | ], 702 | ) 703 | 704 | LightningNetworkInvoice = generate_labeler_serializer( 705 | name="LightningNetworkInvoice", 706 | klass=dataclasses.LightningNetworkInvoice, 707 | labels=["invoice_hash", "invoice", "_PLACEHOLDER", "_PLACEHOLDER", "amount"], 708 | ) 709 | 710 | Movement = generate_labeler_serializer( 711 | name="Movement", 712 | klass=dataclasses.Movement, 713 | labels=[ 714 | "id", 715 | "currency", 716 | "currency_name", 717 | "_PLACEHOLDER", 718 | "_PLACEHOLDER", 719 | "mts_start", 720 | "mts_update", 721 | "_PLACEHOLDER", 722 | "_PLACEHOLDER", 723 | "status", 724 | "_PLACEHOLDER", 725 | "_PLACEHOLDER", 726 | "amount", 727 | "fees", 728 | "_PLACEHOLDER", 729 | "_PLACEHOLDER", 730 | "destination_address", 731 | "_PLACEHOLDER", 732 | "_PLACEHOLDER", 733 | "_PLACEHOLDER", 734 | "transaction_id", 735 | "withdraw_transaction_note", 736 | ], 737 | ) 738 | 739 | SymbolMarginInfo = generate_labeler_serializer( 740 | name="SymbolMarginInfo", 741 | klass=dataclasses.SymbolMarginInfo, 742 | labels=[ 743 | "_PLACEHOLDER", 744 | "symbol", 745 | "tradable_balance", 746 | "gross_balance", 747 | "buy", 748 | "sell", 749 | ], 750 | flat=True, 751 | ) 752 | 753 | BaseMarginInfo = generate_labeler_serializer( 754 | name="BaseMarginInfo", 755 | klass=dataclasses.BaseMarginInfo, 756 | labels=[ 757 | "_PLACEHOLDER", 758 | "user_pl", 759 | "user_swaps", 760 | "margin_balance", 761 | "margin_net", 762 | "margin_min", 763 | ], 764 | flat=True, 765 | ) 766 | 767 | PositionClaim = generate_labeler_serializer( 768 | name="PositionClaim", 769 | klass=dataclasses.PositionClaim, 770 | labels=[ 771 | "symbol", 772 | "position_status", 773 | "amount", 774 | "base_price", 775 | "margin_funding", 776 | "margin_funding_type", 777 | "_PLACEHOLDER", 778 | "_PLACEHOLDER", 779 | "_PLACEHOLDER", 780 | "_PLACEHOLDER", 781 | "_PLACEHOLDER", 782 | "position_id", 783 | "mts_create", 784 | "mts_update", 785 | "_PLACEHOLDER", 786 | "pos_type", 787 | "_PLACEHOLDER", 788 | "collateral", 789 | "min_collateral", 790 | "meta", 791 | ], 792 | ) 793 | 794 | PositionIncreaseInfo = generate_labeler_serializer( 795 | name="PositionIncreaseInfo", 796 | klass=dataclasses.PositionIncreaseInfo, 797 | labels=[ 798 | "max_pos", 799 | "current_pos", 800 | "base_currency_balance", 801 | "tradable_balance_quote_currency", 802 | "tradable_balance_quote_total", 803 | "tradable_balance_base_currency", 804 | "tradable_balance_base_total", 805 | "_PLACEHOLDER", 806 | "_PLACEHOLDER", 807 | "_PLACEHOLDER", 808 | "_PLACEHOLDER", 809 | "funding_avail", 810 | "_PLACEHOLDER", 811 | "_PLACEHOLDER", 812 | "funding_value", 813 | "funding_required", 814 | "funding_value_currency", 815 | "funding_required_currency", 816 | ], 817 | flat=True, 818 | ) 819 | 820 | PositionIncrease = generate_labeler_serializer( 821 | name="PositionIncrease", 822 | klass=dataclasses.PositionIncrease, 823 | labels=["symbol", "_PLACEHOLDER", "amount", "base_price"], 824 | ) 825 | 826 | PositionHistory = generate_labeler_serializer( 827 | name="PositionHistory", 828 | klass=dataclasses.PositionHistory, 829 | labels=[ 830 | "symbol", 831 | "status", 832 | "amount", 833 | "base_price", 834 | "funding", 835 | "funding_type", 836 | "_PLACEHOLDER", 837 | "_PLACEHOLDER", 838 | "_PLACEHOLDER", 839 | "_PLACEHOLDER", 840 | "_PLACEHOLDER", 841 | "position_id", 842 | "mts_create", 843 | "mts_update", 844 | ], 845 | ) 846 | 847 | PositionSnapshot = generate_labeler_serializer( 848 | name="PositionSnapshot", 849 | klass=dataclasses.PositionSnapshot, 850 | labels=[ 851 | "symbol", 852 | "status", 853 | "amount", 854 | "base_price", 855 | "funding", 856 | "funding_type", 857 | "_PLACEHOLDER", 858 | "_PLACEHOLDER", 859 | "_PLACEHOLDER", 860 | "_PLACEHOLDER", 861 | "_PLACEHOLDER", 862 | "position_id", 863 | "mts_create", 864 | "mts_update", 865 | ], 866 | ) 867 | 868 | PositionAudit = generate_labeler_serializer( 869 | name="PositionAudit", 870 | klass=dataclasses.PositionAudit, 871 | labels=[ 872 | "symbol", 873 | "status", 874 | "amount", 875 | "base_price", 876 | "funding", 877 | "funding_type", 878 | "_PLACEHOLDER", 879 | "_PLACEHOLDER", 880 | "_PLACEHOLDER", 881 | "_PLACEHOLDER", 882 | "_PLACEHOLDER", 883 | "position_id", 884 | "mts_create", 885 | "mts_update", 886 | "_PLACEHOLDER", 887 | "type", 888 | "_PLACEHOLDER", 889 | "collateral", 890 | "collateral_min", 891 | "meta", 892 | ], 893 | ) 894 | 895 | DerivativePositionCollateral = generate_labeler_serializer( 896 | name="DerivativePositionCollateral", 897 | klass=dataclasses.DerivativePositionCollateral, 898 | labels=["status"], 899 | ) 900 | 901 | DerivativePositionCollateralLimits = generate_labeler_serializer( 902 | name="DerivativePositionCollateralLimits", 903 | klass=dataclasses.DerivativePositionCollateralLimits, 904 | labels=["min_collateral", "max_collateral"], 905 | ) 906 | 907 | BalanceInfo = generate_labeler_serializer( 908 | name="BalanceInfo", 909 | klass=dataclasses.BalanceInfo, 910 | labels=["aum", "aum_net"], 911 | ) 912 | 913 | # endregion 914 | -------------------------------------------------------------------------------- /bfxapi/websocket/__init__.py: -------------------------------------------------------------------------------- 1 | from ._client import BfxWebSocketClient 2 | -------------------------------------------------------------------------------- /bfxapi/websocket/_client/__init__.py: -------------------------------------------------------------------------------- 1 | from .bfx_websocket_client import BfxWebSocketClient 2 | -------------------------------------------------------------------------------- /bfxapi/websocket/_client/bfx_websocket_bucket.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | import uuid 4 | from typing import Any, Dict, List, Optional, cast 5 | 6 | import websockets.client 7 | from pyee import EventEmitter 8 | 9 | from bfxapi._utils.json_decoder import JSONDecoder 10 | from bfxapi.websocket._connection import Connection 11 | from bfxapi.websocket._handlers import PublicChannelsHandler 12 | from bfxapi.websocket.subscriptions import Subscription 13 | 14 | _CHECKSUM_FLAG_VALUE = 131_072 15 | 16 | 17 | def _strip(message: Dict[str, Any], keys: List[str]) -> Dict[str, Any]: 18 | return {key: value for key, value in message.items() if key not in keys} 19 | 20 | 21 | class BfxWebSocketBucket(Connection): 22 | __MAXIMUM_SUBSCRIPTIONS_AMOUNT = 25 23 | 24 | def __init__(self, host: str, event_emitter: EventEmitter) -> None: 25 | super().__init__(host) 26 | 27 | self.__event_emitter = event_emitter 28 | self.__pendings: List[Dict[str, Any]] = [] 29 | self.__subscriptions: Dict[int, Subscription] = {} 30 | 31 | self.__condition = asyncio.locks.Condition() 32 | 33 | self.__handler = PublicChannelsHandler(event_emitter=self.__event_emitter) 34 | 35 | @property 36 | def count(self) -> int: 37 | return len(self.__pendings) + len(self.__subscriptions) 38 | 39 | @property 40 | def is_full(self) -> bool: 41 | return self.count == BfxWebSocketBucket.__MAXIMUM_SUBSCRIPTIONS_AMOUNT 42 | 43 | @property 44 | def ids(self) -> List[str]: 45 | return [pending["subId"] for pending in self.__pendings] + [ 46 | subscription["sub_id"] for subscription in self.__subscriptions.values() 47 | ] 48 | 49 | async def start(self) -> None: 50 | async with websockets.client.connect(self._host) as websocket: 51 | self._websocket = websocket 52 | 53 | await self.__recover_state() 54 | 55 | async with self.__condition: 56 | self.__condition.notify(1) 57 | 58 | async for _message in self._websocket: 59 | message = json.loads(_message, cls=JSONDecoder) 60 | 61 | if isinstance(message, dict): 62 | if message["event"] == "subscribed": 63 | self.__on_subscribed(message) 64 | 65 | if isinstance(message, list): 66 | if ( 67 | (chan_id := cast(int, message[0])) 68 | and (subscription := self.__subscriptions.get(chan_id)) 69 | and (message[1] != Connection._HEARTBEAT) 70 | ): 71 | self.__handler.handle(subscription, message[1:]) 72 | 73 | def __on_subscribed(self, message: Dict[str, Any]) -> None: 74 | chan_id = cast(int, message["chan_id"]) 75 | 76 | subscription = cast( 77 | Subscription, _strip(message, keys=["chan_id", "event", "pair", "currency"]) 78 | ) 79 | 80 | self.__pendings = [ 81 | pending 82 | for pending in self.__pendings 83 | if pending["subId"] != message["sub_id"] 84 | ] 85 | 86 | self.__subscriptions[chan_id] = subscription 87 | 88 | self.__event_emitter.emit("subscribed", subscription) 89 | 90 | async def __recover_state(self) -> None: 91 | for pending in self.__pendings: 92 | await self._websocket.send(message=json.dumps(pending)) 93 | 94 | for chan_id in list(self.__subscriptions.keys()): 95 | subscription = self.__subscriptions.pop(chan_id) 96 | 97 | await self.subscribe(**subscription) 98 | 99 | await self.__set_config([_CHECKSUM_FLAG_VALUE]) 100 | 101 | async def __set_config(self, flags: List[int]) -> None: 102 | await self._websocket.send(json.dumps({"event": "conf", "flags": sum(flags)})) 103 | 104 | @Connection._require_websocket_connection 105 | async def subscribe( 106 | self, channel: str, sub_id: Optional[str] = None, **kwargs: Any 107 | ) -> None: 108 | subscription: Dict[str, Any] = { 109 | **kwargs, 110 | "event": "subscribe", 111 | "channel": channel, 112 | } 113 | 114 | subscription["subId"] = sub_id or str(uuid.uuid4()) 115 | 116 | self.__pendings.append(subscription) 117 | 118 | await self._websocket.send(message=json.dumps(subscription)) 119 | 120 | @Connection._require_websocket_connection 121 | async def unsubscribe(self, sub_id: str) -> None: 122 | for chan_id, subscription in list(self.__subscriptions.items()): 123 | if subscription["sub_id"] == sub_id: 124 | unsubscription = {"event": "unsubscribe", "chanId": chan_id} 125 | 126 | del self.__subscriptions[chan_id] 127 | 128 | await self._websocket.send(message=json.dumps(unsubscription)) 129 | 130 | @Connection._require_websocket_connection 131 | async def resubscribe(self, sub_id: str) -> None: 132 | for subscription in list(self.__subscriptions.values()): 133 | if subscription["sub_id"] == sub_id: 134 | await self.unsubscribe(sub_id) 135 | 136 | await self.subscribe(**subscription) 137 | 138 | @Connection._require_websocket_connection 139 | async def close(self, code: int = 1000, reason: str = "") -> None: 140 | await self._websocket.close(code, reason) 141 | 142 | def has(self, sub_id: str) -> bool: 143 | for subscription in self.__subscriptions.values(): 144 | if subscription["sub_id"] == sub_id: 145 | return True 146 | 147 | return False 148 | 149 | async def wait(self) -> None: 150 | async with self.__condition: 151 | await self.__condition.wait_for(lambda: self.open) 152 | -------------------------------------------------------------------------------- /bfxapi/websocket/_client/bfx_websocket_client.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | import random 4 | import traceback 5 | from asyncio import Task 6 | from datetime import datetime 7 | from logging import Logger 8 | from socket import gaierror 9 | from typing import Any, Dict, List, Optional, TypedDict 10 | 11 | import websockets 12 | import websockets.client 13 | from websockets.exceptions import ConnectionClosedError, InvalidStatusCode 14 | 15 | from bfxapi._utils.json_encoder import JSONEncoder 16 | from bfxapi.exceptions import InvalidCredentialError 17 | from bfxapi.websocket._connection import Connection 18 | from bfxapi.websocket._event_emitter import BfxEventEmitter 19 | from bfxapi.websocket._handlers import AuthEventsHandler 20 | from bfxapi.websocket.exceptions import ( 21 | ReconnectionTimeoutError, 22 | SubIdError, 23 | UnknownChannelError, 24 | UnknownSubscriptionError, 25 | VersionMismatchError, 26 | ) 27 | 28 | from .bfx_websocket_bucket import BfxWebSocketBucket 29 | from .bfx_websocket_inputs import BfxWebSocketInputs 30 | 31 | _Credentials = TypedDict( 32 | "_Credentials", {"api_key": str, "api_secret": str, "filters": Optional[List[str]]} 33 | ) 34 | 35 | _Reconnection = TypedDict( 36 | "_Reconnection", {"attempts": int, "reason": str, "timestamp": datetime} 37 | ) 38 | 39 | _DEFAULT_LOGGER = Logger("bfxapi.websocket._client", level=0) 40 | 41 | 42 | class _Delay: 43 | __BACKOFF_MIN = 1.92 44 | 45 | __BACKOFF_MAX = 60.0 46 | 47 | def __init__(self, backoff_factor: float) -> None: 48 | self.__backoff_factor = backoff_factor 49 | self.__backoff_delay = _Delay.__BACKOFF_MIN 50 | self.__initial_delay = random.uniform(1.0, 5.0) 51 | 52 | def next(self) -> float: 53 | _backoff_delay = self.peek() 54 | __backoff_delay = _backoff_delay * self.__backoff_factor 55 | self.__backoff_delay = min(__backoff_delay, _Delay.__BACKOFF_MAX) 56 | 57 | return _backoff_delay 58 | 59 | def peek(self) -> float: 60 | return ( 61 | (self.__backoff_delay == _Delay.__BACKOFF_MIN) 62 | and self.__initial_delay 63 | or self.__backoff_delay 64 | ) 65 | 66 | def reset(self) -> None: 67 | self.__backoff_delay = _Delay.__BACKOFF_MIN 68 | 69 | 70 | class BfxWebSocketClient(Connection): 71 | def __init__( 72 | self, 73 | host: str, 74 | *, 75 | credentials: Optional[_Credentials] = None, 76 | timeout: Optional[int] = 60 * 15, 77 | logger: Logger = _DEFAULT_LOGGER, 78 | ) -> None: 79 | super().__init__(host) 80 | 81 | self.__credentials, self.__timeout, self.__logger = credentials, timeout, logger 82 | 83 | self.__buckets: Dict[BfxWebSocketBucket, Optional[Task]] = {} 84 | 85 | self.__reconnection: Optional[_Reconnection] = None 86 | 87 | self.__event_emitter = BfxEventEmitter(loop=None) 88 | 89 | self.__handler = AuthEventsHandler(event_emitter=self.__event_emitter) 90 | 91 | self.__inputs = BfxWebSocketInputs( 92 | handle_websocket_input=self.__handle_websocket_input 93 | ) 94 | 95 | @self.__event_emitter.listens_to("error") 96 | def error(exception: Exception) -> None: 97 | header = f"{type(exception).__name__}: {str(exception)}" 98 | 99 | stack_trace = traceback.format_exception( 100 | type(exception), exception, exception.__traceback__ 101 | ) 102 | 103 | self.__logger.critical(f"{header}\n" + str().join(stack_trace)[:-1]) 104 | 105 | @property 106 | def inputs(self) -> BfxWebSocketInputs: 107 | return self.__inputs 108 | 109 | def run(self) -> None: 110 | return asyncio.get_event_loop().run_until_complete(self.start()) 111 | 112 | async def start(self) -> None: 113 | _delay = _Delay(backoff_factor=1.618) 114 | 115 | _sleep: Optional[Task] = None 116 | 117 | def _on_timeout(): 118 | if not self.open: 119 | if _sleep: 120 | _sleep.cancel() 121 | 122 | while True: 123 | if self.__reconnection: 124 | _sleep = asyncio.create_task(asyncio.sleep(int(_delay.next()))) 125 | 126 | try: 127 | await _sleep 128 | except asyncio.CancelledError: 129 | raise ReconnectionTimeoutError( 130 | "Connection has been offline for too long " 131 | f"without being able to reconnect (timeout: {self.__timeout}s)." 132 | ) from None 133 | 134 | try: 135 | await self.__connect() 136 | except (ConnectionClosedError, InvalidStatusCode, gaierror) as error: 137 | 138 | async def _cancel(task: Task) -> None: 139 | task.cancel() 140 | 141 | try: 142 | await task 143 | except (ConnectionClosedError, InvalidStatusCode, gaierror) as _e: 144 | nonlocal error 145 | 146 | if type(error) is not type(_e) or error.args != _e.args: 147 | raise _e 148 | except asyncio.CancelledError: 149 | pass 150 | 151 | for bucket in self.__buckets: 152 | if task := self.__buckets[bucket]: 153 | self.__buckets[bucket] = None 154 | 155 | await _cancel(task) 156 | 157 | if isinstance(error, ConnectionClosedError) and error.code in ( 158 | 1006, 159 | 1012, 160 | ): 161 | if error.code == 1006: 162 | self.__logger.error("Connection lost: trying to reconnect...") 163 | 164 | if error.code == 1012: 165 | self.__logger.warning( 166 | "WSS server is restarting: all " 167 | "clients need to reconnect (server sent 20051)." 168 | ) 169 | 170 | if self.__timeout: 171 | asyncio.get_event_loop().call_later(self.__timeout, _on_timeout) 172 | 173 | self.__reconnection = { 174 | "attempts": 1, 175 | "reason": error.reason, 176 | "timestamp": datetime.now(), 177 | } 178 | 179 | self._authentication = False 180 | 181 | _delay.reset() 182 | elif ( 183 | (isinstance(error, InvalidStatusCode) and error.status_code == 408) 184 | or isinstance(error, gaierror) 185 | ) and self.__reconnection: 186 | self.__logger.warning( 187 | "Reconnection attempt unsuccessful (no." 188 | f"{self.__reconnection['attempts']}): next attempt in " 189 | f"~{int(_delay.peek())}.0s." 190 | ) 191 | 192 | self.__logger.info( 193 | f"The client has been offline for " 194 | f"{datetime.now() - self.__reconnection['timestamp']}." 195 | ) 196 | 197 | self.__reconnection["attempts"] += 1 198 | else: 199 | raise error 200 | 201 | if not self.__reconnection: 202 | self.__event_emitter.emit( 203 | "disconnected", 204 | self._websocket.close_code, 205 | self._websocket.close_reason, 206 | ) 207 | 208 | break 209 | 210 | async def __connect(self) -> None: 211 | async with websockets.client.connect(self._host) as websocket: 212 | if self.__reconnection: 213 | self.__logger.warning( 214 | "Reconnection attempt successful (no." 215 | f"{self.__reconnection['attempts']}): recovering " 216 | "connection state..." 217 | ) 218 | 219 | self.__reconnection = None 220 | 221 | self._websocket = websocket 222 | 223 | for bucket in self.__buckets: 224 | self.__buckets[bucket] = asyncio.create_task(bucket.start()) 225 | 226 | if len(self.__buckets) == 0 or ( 227 | await asyncio.gather(*[bucket.wait() for bucket in self.__buckets]) 228 | ): 229 | self.__event_emitter.emit("open") 230 | 231 | if self.__credentials: 232 | authentication = Connection._get_authentication_message( 233 | **self.__credentials 234 | ) 235 | 236 | await self._websocket.send(authentication) 237 | 238 | async for _message in self._websocket: 239 | message = json.loads(_message) 240 | 241 | if isinstance(message, dict): 242 | if message["event"] == "info" and "version" in message: 243 | if message["version"] != 2: 244 | raise VersionMismatchError( 245 | "Mismatch between the client and the server version: " 246 | "please update bitfinex-api-py to the latest version " 247 | f"to resolve this error (client version: 2, server " 248 | f"version: {message['version']})." 249 | ) 250 | elif message["event"] == "info" and message["code"] == 20051: 251 | rcvd = websockets.frames.Close( 252 | 1012, "Stop/Restart WebSocket Server (please reconnect)." 253 | ) 254 | 255 | raise ConnectionClosedError(rcvd=rcvd, sent=None) 256 | elif message["event"] == "auth": 257 | if message["status"] != "OK": 258 | raise InvalidCredentialError( 259 | "Can't authenticate with given API-KEY and API-SECRET." 260 | ) 261 | 262 | self.__event_emitter.emit("authenticated", message) 263 | 264 | self._authentication = True 265 | 266 | if ( 267 | isinstance(message, list) 268 | and message[0] == 0 269 | and message[1] != Connection._HEARTBEAT 270 | ): 271 | self.__handler.handle(message[1], message[2]) 272 | 273 | async def __new_bucket(self) -> BfxWebSocketBucket: 274 | bucket = BfxWebSocketBucket(self._host, self.__event_emitter) 275 | 276 | self.__buckets[bucket] = asyncio.create_task(bucket.start()) 277 | 278 | await bucket.wait() 279 | 280 | return bucket 281 | 282 | @Connection._require_websocket_connection 283 | async def subscribe( 284 | self, channel: str, sub_id: Optional[str] = None, **kwargs: Any 285 | ) -> None: 286 | if channel not in ["ticker", "trades", "book", "candles", "status"]: 287 | raise UnknownChannelError( 288 | "Available channels are: ticker, trades, book, candles and status." 289 | ) 290 | 291 | for bucket in self.__buckets: 292 | if sub_id in bucket.ids: 293 | raise SubIdError("sub_id must be unique for all subscriptions.") 294 | 295 | for bucket in self.__buckets: 296 | if not bucket.is_full: 297 | return await bucket.subscribe(channel, sub_id, **kwargs) 298 | 299 | bucket = await self.__new_bucket() 300 | 301 | return await bucket.subscribe(channel, sub_id, **kwargs) 302 | 303 | @Connection._require_websocket_connection 304 | async def unsubscribe(self, sub_id: str) -> None: 305 | for bucket in self.__buckets: 306 | if bucket.has(sub_id): 307 | if bucket.count == 1: 308 | del self.__buckets[bucket] 309 | 310 | return await bucket.close(code=1001, reason="Going Away") 311 | 312 | return await bucket.unsubscribe(sub_id) 313 | 314 | raise UnknownSubscriptionError( 315 | f"Unable to find a subscription with sub_id <{sub_id}>." 316 | ) 317 | 318 | @Connection._require_websocket_connection 319 | async def resubscribe(self, sub_id: str) -> None: 320 | for bucket in self.__buckets: 321 | if bucket.has(sub_id): 322 | return await bucket.resubscribe(sub_id) 323 | 324 | raise UnknownSubscriptionError( 325 | f"Unable to find a subscription with sub_id <{sub_id}>." 326 | ) 327 | 328 | @Connection._require_websocket_connection 329 | async def close(self, code: int = 1000, reason: str = "") -> None: 330 | for bucket in self.__buckets: 331 | await bucket.close(code=code, reason=reason) 332 | 333 | if self._websocket.open: 334 | await self._websocket.close(code=code, reason=reason) 335 | 336 | @Connection._require_websocket_authentication 337 | async def notify( 338 | self, info: Any, message_id: Optional[int] = None, **kwargs: Any 339 | ) -> None: 340 | await self._websocket.send( 341 | json.dumps( 342 | [0, "n", message_id, {"type": "ucm-test", "info": info, **kwargs}] 343 | ) 344 | ) 345 | 346 | @Connection._require_websocket_authentication 347 | async def __handle_websocket_input(self, event: str, data: Any) -> None: 348 | await self._websocket.send(json.dumps([0, event, None, data], cls=JSONEncoder)) 349 | 350 | def on(self, event, callback=None): 351 | return self.__event_emitter.on(event, callback) 352 | -------------------------------------------------------------------------------- /bfxapi/websocket/_client/bfx_websocket_inputs.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | from typing import Any, Awaitable, Callable, Dict, List, Optional, Tuple, Union 3 | 4 | _Handler = Callable[[str, Any], Awaitable[None]] 5 | 6 | 7 | class BfxWebSocketInputs: 8 | def __init__(self, handle_websocket_input: _Handler) -> None: 9 | self.__handle_websocket_input = handle_websocket_input 10 | 11 | async def submit_order( 12 | self, 13 | type: str, 14 | symbol: str, 15 | amount: Union[str, float, Decimal], 16 | price: Union[str, float, Decimal], 17 | *, 18 | lev: Optional[int] = None, 19 | price_trailing: Optional[Union[str, float, Decimal]] = None, 20 | price_aux_limit: Optional[Union[str, float, Decimal]] = None, 21 | price_oco_stop: Optional[Union[str, float, Decimal]] = None, 22 | gid: Optional[int] = None, 23 | cid: Optional[int] = None, 24 | flags: Optional[int] = None, 25 | tif: Optional[str] = None, 26 | meta: Optional[Dict[str, Any]] = None, 27 | ) -> None: 28 | await self.__handle_websocket_input( 29 | "on", 30 | { 31 | "type": type, 32 | "symbol": symbol, 33 | "amount": amount, 34 | "price": price, 35 | "lev": lev, 36 | "price_trailing": price_trailing, 37 | "price_aux_limit": price_aux_limit, 38 | "price_oco_stop": price_oco_stop, 39 | "gid": gid, 40 | "cid": cid, 41 | "flags": flags, 42 | "tif": tif, 43 | "meta": meta, 44 | }, 45 | ) 46 | 47 | async def update_order( 48 | self, 49 | id: int, 50 | *, 51 | amount: Optional[Union[str, float, Decimal]] = None, 52 | price: Optional[Union[str, float, Decimal]] = None, 53 | cid: Optional[int] = None, 54 | cid_date: Optional[str] = None, 55 | gid: Optional[int] = None, 56 | flags: Optional[int] = None, 57 | lev: Optional[int] = None, 58 | delta: Optional[Union[str, float, Decimal]] = None, 59 | price_aux_limit: Optional[Union[str, float, Decimal]] = None, 60 | price_trailing: Optional[Union[str, float, Decimal]] = None, 61 | tif: Optional[str] = None, 62 | ) -> None: 63 | await self.__handle_websocket_input( 64 | "ou", 65 | { 66 | "id": id, 67 | "amount": amount, 68 | "price": price, 69 | "cid": cid, 70 | "cid_date": cid_date, 71 | "gid": gid, 72 | "flags": flags, 73 | "lev": lev, 74 | "delta": delta, 75 | "price_aux_limit": price_aux_limit, 76 | "price_trailing": price_trailing, 77 | "tif": tif, 78 | }, 79 | ) 80 | 81 | async def cancel_order( 82 | self, 83 | *, 84 | id: Optional[int] = None, 85 | cid: Optional[int] = None, 86 | cid_date: Optional[str] = None, 87 | ) -> None: 88 | await self.__handle_websocket_input( 89 | "oc", {"id": id, "cid": cid, "cid_date": cid_date} 90 | ) 91 | 92 | async def cancel_order_multi( 93 | self, 94 | *, 95 | id: Optional[List[int]] = None, 96 | cid: Optional[List[Tuple[int, str]]] = None, 97 | gid: Optional[List[int]] = None, 98 | all: Optional[bool] = None, 99 | ) -> None: 100 | await self.__handle_websocket_input( 101 | "oc_multi", {"id": id, "cid": cid, "gid": gid, "all": all} 102 | ) 103 | 104 | async def submit_funding_offer( 105 | self, 106 | type: str, 107 | symbol: str, 108 | amount: Union[str, float, Decimal], 109 | rate: Union[str, float, Decimal], 110 | period: int, 111 | *, 112 | flags: Optional[int] = None, 113 | ) -> None: 114 | await self.__handle_websocket_input( 115 | "fon", 116 | { 117 | "type": type, 118 | "symbol": symbol, 119 | "amount": amount, 120 | "rate": rate, 121 | "period": period, 122 | "flags": flags, 123 | }, 124 | ) 125 | 126 | async def cancel_funding_offer(self, id: int) -> None: 127 | await self.__handle_websocket_input("foc", {"id": id}) 128 | 129 | async def calc(self, *args: str) -> None: 130 | await self.__handle_websocket_input("calc", list(map(lambda arg: [arg], args))) 131 | -------------------------------------------------------------------------------- /bfxapi/websocket/_connection.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import hmac 3 | import json 4 | from abc import ABC, abstractmethod 5 | from datetime import datetime 6 | from functools import wraps 7 | from typing import Any, Awaitable, Callable, Dict, List, Optional, TypeVar, cast 8 | 9 | from typing_extensions import Concatenate, ParamSpec 10 | from websockets.client import WebSocketClientProtocol 11 | 12 | from bfxapi.websocket.exceptions import ActionRequiresAuthentication, ConnectionNotOpen 13 | 14 | _S = TypeVar("_S", bound="Connection") 15 | 16 | _R = TypeVar("_R") 17 | 18 | _P = ParamSpec("_P") 19 | 20 | 21 | class Connection(ABC): 22 | _HEARTBEAT = "hb" 23 | 24 | def __init__(self, host: str) -> None: 25 | self._host = host 26 | 27 | self._authentication: bool = False 28 | 29 | self.__protocol: Optional[WebSocketClientProtocol] = None 30 | 31 | @property 32 | def open(self) -> bool: 33 | return self.__protocol is not None and self.__protocol.open 34 | 35 | @property 36 | def authentication(self) -> bool: 37 | return self._authentication 38 | 39 | @property 40 | def _websocket(self) -> WebSocketClientProtocol: 41 | return cast(WebSocketClientProtocol, self.__protocol) 42 | 43 | @_websocket.setter 44 | def _websocket(self, protocol: WebSocketClientProtocol) -> None: 45 | self.__protocol = protocol 46 | 47 | @abstractmethod 48 | async def start(self) -> None: ... 49 | 50 | @staticmethod 51 | def _require_websocket_connection( 52 | function: Callable[Concatenate[_S, _P], Awaitable[_R]], 53 | ) -> Callable[Concatenate[_S, _P], Awaitable[_R]]: 54 | @wraps(function) 55 | async def wrapper(self: _S, *args: Any, **kwargs: Any) -> _R: 56 | if self.open: 57 | return await function(self, *args, **kwargs) 58 | 59 | raise ConnectionNotOpen("No open connection with the server.") 60 | 61 | return wrapper 62 | 63 | @staticmethod 64 | def _require_websocket_authentication( 65 | function: Callable[Concatenate[_S, _P], Awaitable[_R]], 66 | ) -> Callable[Concatenate[_S, _P], Awaitable[_R]]: 67 | @wraps(function) 68 | async def wrapper(self: _S, *args: Any, **kwargs: Any) -> _R: 69 | if not self.authentication: 70 | raise ActionRequiresAuthentication( 71 | "To perform this action you need to " 72 | "authenticate using your API_KEY and API_SECRET." 73 | ) 74 | 75 | internal = Connection._require_websocket_connection(function) 76 | 77 | return await internal(self, *args, **kwargs) 78 | 79 | return wrapper 80 | 81 | @staticmethod 82 | def _get_authentication_message( 83 | api_key: str, api_secret: str, filters: Optional[List[str]] = None 84 | ) -> str: 85 | message: Dict[str, Any] = { 86 | "event": "auth", 87 | "filter": filters, 88 | "apiKey": api_key, 89 | } 90 | 91 | message["authNonce"] = round(datetime.now().timestamp() * 1_000_000) 92 | 93 | message["authPayload"] = f"AUTH{message['authNonce']}" 94 | 95 | auth_sig = hmac.new( 96 | key=api_secret.encode("utf8"), 97 | msg=message["authPayload"].encode("utf8"), 98 | digestmod=hashlib.sha384, 99 | ) 100 | 101 | message["authSig"] = auth_sig.hexdigest() 102 | 103 | return json.dumps(message) 104 | -------------------------------------------------------------------------------- /bfxapi/websocket/_event_emitter/__init__.py: -------------------------------------------------------------------------------- 1 | from .bfx_event_emitter import BfxEventEmitter 2 | -------------------------------------------------------------------------------- /bfxapi/websocket/_event_emitter/bfx_event_emitter.py: -------------------------------------------------------------------------------- 1 | from asyncio import AbstractEventLoop 2 | from collections import defaultdict 3 | from typing import Any, Callable, Dict, List, Optional, TypeVar, Union 4 | 5 | from pyee.asyncio import AsyncIOEventEmitter 6 | 7 | from bfxapi.websocket.exceptions import UnknownEventError 8 | 9 | _Handler = TypeVar("_Handler", bound=Callable[..., None]) 10 | 11 | _ONCE_PER_CONNECTION = [ 12 | "open", 13 | "authenticated", 14 | "order_snapshot", 15 | "position_snapshot", 16 | "funding_offer_snapshot", 17 | "funding_credit_snapshot", 18 | "funding_loan_snapshot", 19 | "wallet_snapshot", 20 | ] 21 | 22 | _ONCE_PER_SUBSCRIPTION = [ 23 | "subscribed", 24 | "t_trades_snapshot", 25 | "f_trades_snapshot", 26 | "t_book_snapshot", 27 | "f_book_snapshot", 28 | "t_raw_book_snapshot", 29 | "f_raw_book_snapshot", 30 | "candles_snapshot", 31 | ] 32 | 33 | _COMMON = [ 34 | "disconnected", 35 | "t_ticker_update", 36 | "f_ticker_update", 37 | "t_trade_execution", 38 | "t_trade_execution_update", 39 | "f_trade_execution", 40 | "f_trade_execution_update", 41 | "t_book_update", 42 | "f_book_update", 43 | "t_raw_book_update", 44 | "f_raw_book_update", 45 | "candles_update", 46 | "derivatives_status_update", 47 | "liquidation_feed_update", 48 | "checksum", 49 | "order_new", 50 | "order_update", 51 | "order_cancel", 52 | "position_new", 53 | "position_update", 54 | "position_close", 55 | "funding_offer_new", 56 | "funding_offer_update", 57 | "funding_offer_cancel", 58 | "funding_credit_new", 59 | "funding_credit_update", 60 | "funding_credit_close", 61 | "funding_loan_new", 62 | "funding_loan_update", 63 | "funding_loan_close", 64 | "trade_execution", 65 | "trade_execution_update", 66 | "wallet_update", 67 | "base_margin_info", 68 | "symbol_margin_info", 69 | "funding_info_update", 70 | "balance_update", 71 | "notification", 72 | "on-req-notification", 73 | "ou-req-notification", 74 | "oc-req-notification", 75 | "fon-req-notification", 76 | "foc-req-notification", 77 | ] 78 | 79 | 80 | class BfxEventEmitter(AsyncIOEventEmitter): 81 | _EVENTS = _ONCE_PER_CONNECTION + _ONCE_PER_SUBSCRIPTION + _COMMON 82 | 83 | def __init__(self, loop: Optional[AbstractEventLoop] = None) -> None: 84 | super().__init__(loop) 85 | 86 | self._connection: List[str] = [] 87 | 88 | self._subscriptions: Dict[str, List[str]] = defaultdict(lambda: []) 89 | 90 | def emit(self, event: str, *args: Any, **kwargs: Any) -> bool: 91 | if event in _ONCE_PER_CONNECTION: 92 | if event in self._connection: 93 | return self._has_listeners(event) 94 | 95 | self._connection += [event] 96 | 97 | if event in _ONCE_PER_SUBSCRIPTION: 98 | sub_id = args[0]["sub_id"] 99 | 100 | if event in self._subscriptions[sub_id]: 101 | return self._has_listeners(event) 102 | 103 | self._subscriptions[sub_id] += [event] 104 | 105 | return super().emit(event, *args, **kwargs) 106 | 107 | def on( 108 | self, event: str, f: Optional[_Handler] = None 109 | ) -> Union[_Handler, Callable[[_Handler], _Handler]]: 110 | if event not in BfxEventEmitter._EVENTS: 111 | raise UnknownEventError( 112 | f"Can't register to unknown event: <{event}> (to get a full " 113 | "list of available events see https://docs.bitfinex.com/)." 114 | ) 115 | 116 | return super().on(event, f) 117 | 118 | def _has_listeners(self, event: str) -> bool: 119 | with self._lock: 120 | listeners = self._events.get(event) 121 | 122 | return bool(listeners) 123 | -------------------------------------------------------------------------------- /bfxapi/websocket/_handlers/__init__.py: -------------------------------------------------------------------------------- 1 | from .auth_events_handler import AuthEventsHandler 2 | from .public_channels_handler import PublicChannelsHandler 3 | -------------------------------------------------------------------------------- /bfxapi/websocket/_handlers/auth_events_handler.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, Tuple 2 | 3 | from pyee.base import EventEmitter 4 | 5 | from bfxapi.types import serializers 6 | from bfxapi.types.dataclasses import FundingOffer, Order 7 | from bfxapi.types.serializers import _Notification 8 | 9 | 10 | class AuthEventsHandler: 11 | __ABBREVIATIONS = { 12 | "os": "order_snapshot", 13 | "on": "order_new", 14 | "ou": "order_update", 15 | "oc": "order_cancel", 16 | "ps": "position_snapshot", 17 | "pn": "position_new", 18 | "pu": "position_update", 19 | "pc": "position_close", 20 | "te": "trade_execution", 21 | "tu": "trade_execution_update", 22 | "fos": "funding_offer_snapshot", 23 | "fon": "funding_offer_new", 24 | "fou": "funding_offer_update", 25 | "foc": "funding_offer_cancel", 26 | "fcs": "funding_credit_snapshot", 27 | "fcn": "funding_credit_new", 28 | "fcu": "funding_credit_update", 29 | "fcc": "funding_credit_close", 30 | "fls": "funding_loan_snapshot", 31 | "fln": "funding_loan_new", 32 | "flu": "funding_loan_update", 33 | "flc": "funding_loan_close", 34 | "ws": "wallet_snapshot", 35 | "wu": "wallet_update", 36 | "fiu": "funding_info_update", 37 | "bu": "balance_update", 38 | } 39 | 40 | __SERIALIZERS: Dict[Tuple[str, ...], serializers._Serializer] = { 41 | ("os", "on", "ou", "oc"): serializers.Order, 42 | ("ps", "pn", "pu", "pc"): serializers.Position, 43 | ("te", "tu"): serializers.Trade, 44 | ("fos", "fon", "fou", "foc"): serializers.FundingOffer, 45 | ("fcs", "fcn", "fcu", "fcc"): serializers.FundingCredit, 46 | ("fls", "fln", "flu", "flc"): serializers.FundingLoan, 47 | ("ws", "wu"): serializers.Wallet, 48 | ("fiu",): serializers.FundingInfo, 49 | ("bu",): serializers.BalanceInfo, 50 | } 51 | 52 | def __init__(self, event_emitter: EventEmitter) -> None: 53 | self.__event_emitter = event_emitter 54 | 55 | def handle(self, abbrevation: str, stream: Any) -> None: 56 | if abbrevation == "n": 57 | self.__notification(stream) 58 | elif abbrevation == "miu": 59 | if stream[0] == "base": 60 | self.__event_emitter.emit( 61 | "base_margin_info", serializers.BaseMarginInfo.parse(*stream) 62 | ) 63 | elif stream[0] == "sym": 64 | self.__event_emitter.emit( 65 | "symbol_margin_info", serializers.SymbolMarginInfo.parse(*stream) 66 | ) 67 | else: 68 | for abbrevations, serializer in AuthEventsHandler.__SERIALIZERS.items(): 69 | if abbrevation in abbrevations: 70 | event = AuthEventsHandler.__ABBREVIATIONS[abbrevation] 71 | 72 | if all(isinstance(sub_stream, list) for sub_stream in stream): 73 | data = [serializer.parse(*sub_stream) for sub_stream in stream] 74 | else: 75 | data = serializer.parse(*stream) 76 | 77 | self.__event_emitter.emit(event, data) 78 | 79 | def __notification(self, stream: Any) -> None: 80 | event: str = "notification" 81 | 82 | serializer: _Notification = _Notification[None](serializer=None) 83 | 84 | if stream[1] in ("on-req", "ou-req", "oc-req"): 85 | event, serializer = f"{stream[1]}-notification", _Notification[Order]( 86 | serializer=serializers.Order 87 | ) 88 | 89 | if stream[1] in ("fon-req", "foc-req"): 90 | event, serializer = f"{stream[1]}-notification", _Notification[ 91 | FundingOffer 92 | ](serializer=serializers.FundingOffer) 93 | 94 | self.__event_emitter.emit(event, serializer.parse(*stream)) 95 | -------------------------------------------------------------------------------- /bfxapi/websocket/_handlers/public_channels_handler.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List, cast 2 | 3 | from pyee.base import EventEmitter 4 | 5 | from bfxapi.types import serializers 6 | from bfxapi.websocket.subscriptions import ( 7 | Book, 8 | Candles, 9 | Status, 10 | Subscription, 11 | Ticker, 12 | Trades, 13 | ) 14 | 15 | _CHECKSUM = "cs" 16 | 17 | 18 | class PublicChannelsHandler: 19 | def __init__(self, event_emitter: EventEmitter) -> None: 20 | self.__event_emitter = event_emitter 21 | 22 | def handle(self, subscription: Subscription, stream: List[Any]) -> None: 23 | if subscription["channel"] == "ticker": 24 | self.__ticker_channel_handler(cast(Ticker, subscription), stream) 25 | elif subscription["channel"] == "trades": 26 | self.__trades_channel_handler(cast(Trades, subscription), stream) 27 | elif subscription["channel"] == "book": 28 | subscription = cast(Book, subscription) 29 | 30 | if stream[0] == _CHECKSUM: 31 | self.__checksum_handler(subscription, stream[1]) 32 | else: 33 | if subscription["prec"] != "R0": 34 | self.__book_channel_handler(subscription, stream) 35 | else: 36 | self.__raw_book_channel_handler(subscription, stream) 37 | elif subscription["channel"] == "candles": 38 | self.__candles_channel_handler(cast(Candles, subscription), stream) 39 | elif subscription["channel"] == "status": 40 | self.__status_channel_handler(cast(Status, subscription), stream) 41 | 42 | def __ticker_channel_handler(self, subscription: Ticker, stream: List[Any]): 43 | if subscription["symbol"].startswith("t"): 44 | return self.__event_emitter.emit( 45 | "t_ticker_update", 46 | subscription, 47 | serializers.TradingPairTicker.parse(*stream[0]), 48 | ) 49 | 50 | if subscription["symbol"].startswith("f"): 51 | return self.__event_emitter.emit( 52 | "f_ticker_update", 53 | subscription, 54 | serializers.FundingCurrencyTicker.parse(*stream[0]), 55 | ) 56 | 57 | def __trades_channel_handler(self, subscription: Trades, stream: List[Any]): 58 | if (event := stream[0]) and event in ["te", "tu", "fte", "ftu"]: 59 | events = { 60 | "te": "t_trade_execution", 61 | "tu": "t_trade_execution_update", 62 | "fte": "f_trade_execution", 63 | "ftu": "f_trade_execution_update", 64 | } 65 | 66 | if subscription["symbol"].startswith("t"): 67 | return self.__event_emitter.emit( 68 | events[event], 69 | subscription, 70 | serializers.TradingPairTrade.parse(*stream[1]), 71 | ) 72 | 73 | if subscription["symbol"].startswith("f"): 74 | return self.__event_emitter.emit( 75 | events[event], 76 | subscription, 77 | serializers.FundingCurrencyTrade.parse(*stream[1]), 78 | ) 79 | 80 | if subscription["symbol"].startswith("t"): 81 | return self.__event_emitter.emit( 82 | "t_trades_snapshot", 83 | subscription, 84 | [ 85 | serializers.TradingPairTrade.parse(*sub_stream) 86 | for sub_stream in stream[0] 87 | ], 88 | ) 89 | 90 | if subscription["symbol"].startswith("f"): 91 | return self.__event_emitter.emit( 92 | "f_trades_snapshot", 93 | subscription, 94 | [ 95 | serializers.FundingCurrencyTrade.parse(*sub_stream) 96 | for sub_stream in stream[0] 97 | ], 98 | ) 99 | 100 | def __book_channel_handler(self, subscription: Book, stream: List[Any]): 101 | if subscription["symbol"].startswith("t"): 102 | if all(isinstance(sub_stream, list) for sub_stream in stream[0]): 103 | return self.__event_emitter.emit( 104 | "t_book_snapshot", 105 | subscription, 106 | [ 107 | serializers.TradingPairBook.parse(*sub_stream) 108 | for sub_stream in stream[0] 109 | ], 110 | ) 111 | 112 | return self.__event_emitter.emit( 113 | "t_book_update", 114 | subscription, 115 | serializers.TradingPairBook.parse(*stream[0]), 116 | ) 117 | 118 | if subscription["symbol"].startswith("f"): 119 | if all(isinstance(sub_stream, list) for sub_stream in stream[0]): 120 | return self.__event_emitter.emit( 121 | "f_book_snapshot", 122 | subscription, 123 | [ 124 | serializers.FundingCurrencyBook.parse(*sub_stream) 125 | for sub_stream in stream[0] 126 | ], 127 | ) 128 | 129 | return self.__event_emitter.emit( 130 | "f_book_update", 131 | subscription, 132 | serializers.FundingCurrencyBook.parse(*stream[0]), 133 | ) 134 | 135 | def __raw_book_channel_handler(self, subscription: Book, stream: List[Any]): 136 | if subscription["symbol"].startswith("t"): 137 | if all(isinstance(sub_stream, list) for sub_stream in stream[0]): 138 | return self.__event_emitter.emit( 139 | "t_raw_book_snapshot", 140 | subscription, 141 | [ 142 | serializers.TradingPairRawBook.parse(*sub_stream) 143 | for sub_stream in stream[0] 144 | ], 145 | ) 146 | 147 | return self.__event_emitter.emit( 148 | "t_raw_book_update", 149 | subscription, 150 | serializers.TradingPairRawBook.parse(*stream[0]), 151 | ) 152 | 153 | if subscription["symbol"].startswith("f"): 154 | if all(isinstance(sub_stream, list) for sub_stream in stream[0]): 155 | return self.__event_emitter.emit( 156 | "f_raw_book_snapshot", 157 | subscription, 158 | [ 159 | serializers.FundingCurrencyRawBook.parse(*sub_stream) 160 | for sub_stream in stream[0] 161 | ], 162 | ) 163 | 164 | return self.__event_emitter.emit( 165 | "f_raw_book_update", 166 | subscription, 167 | serializers.FundingCurrencyRawBook.parse(*stream[0]), 168 | ) 169 | 170 | def __candles_channel_handler(self, subscription: Candles, stream: List[Any]): 171 | if all(isinstance(sub_stream, list) for sub_stream in stream[0]): 172 | return self.__event_emitter.emit( 173 | "candles_snapshot", 174 | subscription, 175 | [serializers.Candle.parse(*sub_stream) for sub_stream in stream[0]], 176 | ) 177 | 178 | return self.__event_emitter.emit( 179 | "candles_update", subscription, serializers.Candle.parse(*stream[0]) 180 | ) 181 | 182 | def __status_channel_handler(self, subscription: Status, stream: List[Any]): 183 | if subscription["key"].startswith("deriv:"): 184 | return self.__event_emitter.emit( 185 | "derivatives_status_update", 186 | subscription, 187 | serializers.DerivativesStatus.parse(*stream[0]), 188 | ) 189 | 190 | if subscription["key"].startswith("liq:"): 191 | return self.__event_emitter.emit( 192 | "liquidation_feed_update", 193 | subscription, 194 | serializers.Liquidation.parse(*stream[0][0]), 195 | ) 196 | 197 | def __checksum_handler(self, subscription: Book, value: int): 198 | return self.__event_emitter.emit("checksum", subscription, value & 0xFFFFFFFF) 199 | -------------------------------------------------------------------------------- /bfxapi/websocket/exceptions.py: -------------------------------------------------------------------------------- 1 | from bfxapi.exceptions import BfxBaseException 2 | 3 | 4 | class ConnectionNotOpen(BfxBaseException): 5 | pass 6 | 7 | 8 | class ActionRequiresAuthentication(BfxBaseException): 9 | pass 10 | 11 | 12 | class ReconnectionTimeoutError(BfxBaseException): 13 | pass 14 | 15 | 16 | class VersionMismatchError(BfxBaseException): 17 | pass 18 | 19 | 20 | class SubIdError(BfxBaseException): 21 | pass 22 | 23 | 24 | class UnknownChannelError(BfxBaseException): 25 | pass 26 | 27 | 28 | class UnknownEventError(BfxBaseException): 29 | pass 30 | 31 | 32 | class UnknownSubscriptionError(BfxBaseException): 33 | pass 34 | -------------------------------------------------------------------------------- /bfxapi/websocket/subscriptions.py: -------------------------------------------------------------------------------- 1 | from typing import Literal, TypedDict, Union 2 | 3 | Subscription = Union["Ticker", "Trades", "Book", "Candles", "Status"] 4 | 5 | Channel = Literal["ticker", "trades", "book", "candles", "status"] 6 | 7 | 8 | class Ticker(TypedDict): 9 | channel: Literal["ticker"] 10 | sub_id: str 11 | symbol: str 12 | 13 | 14 | class Trades(TypedDict): 15 | channel: Literal["trades"] 16 | sub_id: str 17 | symbol: str 18 | 19 | 20 | class Book(TypedDict): 21 | channel: Literal["book"] 22 | sub_id: str 23 | symbol: str 24 | prec: Literal["R0", "P0", "P1", "P2", "P3", "P4"] 25 | freq: Literal["F0", "F1"] 26 | len: Literal["1", "25", "100", "250"] 27 | 28 | 29 | class Candles(TypedDict): 30 | channel: Literal["candles"] 31 | sub_id: str 32 | key: str 33 | 34 | 35 | class Status(TypedDict): 36 | channel: Literal["status"] 37 | sub_id: str 38 | key: str 39 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitfinexcom/bitfinex-api-py/791c84591f354914784643eed8fd1ac92c98c536/dev-requirements.txt -------------------------------------------------------------------------------- /examples/rest/auth/claim_position.py: -------------------------------------------------------------------------------- 1 | # python -c "import examples.rest.auth.claim_position" 2 | 3 | import os 4 | 5 | from bfxapi import Client 6 | from bfxapi.types import Notification, PositionClaim 7 | 8 | bfx = Client(api_key=os.getenv("BFX_API_KEY"), api_secret=os.getenv("BFX_API_SECRET")) 9 | 10 | # Claims all active positions 11 | for position in bfx.rest.auth.get_positions(): 12 | notification: Notification[PositionClaim] = bfx.rest.auth.claim_position( 13 | position.position_id 14 | ) 15 | claim: PositionClaim = notification.data 16 | print(f"Position: {position} | PositionClaim: {claim}") 17 | -------------------------------------------------------------------------------- /examples/rest/auth/get_wallets.py: -------------------------------------------------------------------------------- 1 | # python -c "import examples.rest.auth.get_wallets" 2 | 3 | import os 4 | from typing import List 5 | 6 | from bfxapi import Client 7 | from bfxapi.types import ( 8 | DepositAddress, 9 | LightningNetworkInvoice, 10 | Notification, 11 | Transfer, 12 | Wallet, 13 | Withdrawal, 14 | ) 15 | 16 | bfx = Client(api_key=os.getenv("BFX_API_KEY"), api_secret=os.getenv("BFX_API_SECRET")) 17 | 18 | # Gets all user's available wallets 19 | wallets: List[Wallet] = bfx.rest.auth.get_wallets() 20 | 21 | # Transfers funds (0.001 ETH) from exchange wallet to funding wallet 22 | A: Notification[Transfer] = bfx.rest.auth.transfer_between_wallets( 23 | from_wallet="exchange", 24 | to_wallet="funding", 25 | currency="ETH", 26 | currency_to="ETH", 27 | amount=0.001, 28 | ) 29 | 30 | print("Transfer:", A.data) 31 | 32 | # Retrieves the deposit address for bitcoin currency in exchange wallet. 33 | B: Notification[DepositAddress] = bfx.rest.auth.get_deposit_address( 34 | wallet="exchange", method="bitcoin", op_renew=False 35 | ) 36 | 37 | print("Deposit address:", B.data) 38 | 39 | # Generates a lightning network deposit invoice 40 | C: Notification[LightningNetworkInvoice] = bfx.rest.auth.generate_deposit_invoice( 41 | wallet="funding", currency="LNX", amount=0.001 42 | ) 43 | 44 | print("Lightning network invoice:", C.data) 45 | 46 | # Withdraws 1.0 UST from user's exchange wallet to address 0x742d35... 47 | D: Notification[Withdrawal] = bfx.rest.auth.submit_wallet_withdrawal( 48 | wallet="exchange", 49 | method="tetheruse", 50 | address="0x742d35Cc6634C0532925a3b844Bc454e4438f44e", 51 | amount=1.0, 52 | ) 53 | 54 | print("Withdrawal:", D.data) 55 | -------------------------------------------------------------------------------- /examples/rest/auth/set_derivative_position_collateral.py: -------------------------------------------------------------------------------- 1 | # python -c "import examples.rest.auth.set_derivative_position_collateral" 2 | 3 | import os 4 | 5 | from bfxapi import Client 6 | from bfxapi.types import ( 7 | DerivativePositionCollateral, 8 | DerivativePositionCollateralLimits, 9 | ) 10 | 11 | bfx = Client(api_key=os.getenv("BFX_API_KEY"), api_secret=os.getenv("BFX_API_SECRET")) 12 | 13 | submit_order_notification = bfx.rest.auth.submit_order( 14 | type="LIMIT", symbol="tBTCF0:USTF0", amount="0.015", price="16700", lev=10 15 | ) 16 | 17 | print("New Order:", submit_order_notification.data) 18 | 19 | # Update the amount of collateral for tBTCF0:USTF0 derivative position 20 | derivative_position_collateral: DerivativePositionCollateral = ( 21 | bfx.rest.auth.set_derivative_position_collateral( 22 | symbol="tBTCF0:USTF0", collateral=50.0 23 | ) 24 | ) 25 | 26 | print("Status:", bool(derivative_position_collateral.status)) 27 | 28 | # Calculate the minimum and maximum collateral that can be assigned to tBTCF0:USTF0. 29 | derivative_position_collateral_limits: DerivativePositionCollateralLimits = ( 30 | bfx.rest.auth.get_derivative_position_collateral_limits(symbol="tBTCF0:USTF0") 31 | ) 32 | 33 | print( 34 | f"Minimum collateral: {derivative_position_collateral_limits.min_collateral} | " 35 | f"Maximum collateral: {derivative_position_collateral_limits.max_collateral}" 36 | ) 37 | -------------------------------------------------------------------------------- /examples/rest/auth/submit_funding_offer.py: -------------------------------------------------------------------------------- 1 | # python -c "import examples.rest.auth.submit_funding_offer" 2 | 3 | import os 4 | 5 | from bfxapi import Client 6 | from bfxapi.types import FundingOffer, Notification 7 | 8 | bfx = Client(api_key=os.getenv("BFX_API_KEY"), api_secret=os.getenv("BFX_API_SECRET")) 9 | 10 | # Submit a new funding offer 11 | notification: Notification[FundingOffer] = bfx.rest.auth.submit_funding_offer( 12 | type="LIMIT", symbol="fUSD", amount=123.45, rate=0.001, period=2 13 | ) 14 | 15 | print("Funding Offer notification:", notification) 16 | 17 | # Get all fUSD active funding offers 18 | offers = bfx.rest.auth.get_funding_offers(symbol="fUSD") 19 | 20 | print("Offers (fUSD):", offers) 21 | -------------------------------------------------------------------------------- /examples/rest/auth/submit_order.py: -------------------------------------------------------------------------------- 1 | # python -c "import examples.rest.auth.submit_order" 2 | 3 | import os 4 | 5 | from bfxapi import Client 6 | from bfxapi.types import Notification, Order 7 | 8 | bfx = Client(api_key=os.getenv("BFX_API_KEY"), api_secret=os.getenv("BFX_API_SECRET")) 9 | 10 | # Submit a new order 11 | submit_order_notification: Notification[Order] = bfx.rest.auth.submit_order( 12 | type="EXCHANGE LIMIT", symbol="tBTCUST", amount=0.015, price=10000 13 | ) 14 | 15 | print("Submit order notification:", submit_order_notification) 16 | 17 | order: Order = submit_order_notification.data 18 | 19 | # Update its amount and its price 20 | update_order_notification: Notification[Order] = bfx.rest.auth.update_order( 21 | id=order.id, amount=0.020, price=10150 22 | ) 23 | 24 | print("Update order notification:", update_order_notification) 25 | 26 | # Cancel it by its ID 27 | cancel_order_notification: Notification[Order] = bfx.rest.auth.cancel_order(id=order.id) 28 | 29 | print("Cancel order notification:", cancel_order_notification) 30 | -------------------------------------------------------------------------------- /examples/rest/auth/toggle_keep_funding.py: -------------------------------------------------------------------------------- 1 | # python -c "import examples.rest.auth.toggle_keep_funding" 2 | 3 | import os 4 | from typing import List 5 | 6 | from bfxapi import Client 7 | from bfxapi.types import FundingLoan, Notification 8 | 9 | bfx = Client(api_key=os.getenv("BFX_API_KEY"), api_secret=os.getenv("BFX_API_SECRET")) 10 | 11 | loans: List[FundingLoan] = bfx.rest.auth.get_funding_loans(symbol="fUSD") 12 | 13 | # Set every loan's keep funding status to (1: , 2: ) 14 | notification: Notification[None] = bfx.rest.auth.toggle_keep_funding( 15 | type="loan", ids=[loan.id for loan in loans], changes={loan.id: 2 for loan in loans} 16 | ) 17 | 18 | print("Toggle keep funding notification:", notification) 19 | -------------------------------------------------------------------------------- /examples/rest/merchant/settings.py: -------------------------------------------------------------------------------- 1 | # python -c "import examples.rest.merchant.settings" 2 | 3 | import os 4 | 5 | from bfxapi import Client 6 | 7 | bfx = Client(api_key=os.getenv("BFX_API_KEY"), api_secret=os.getenv("BFX_API_SECRET")) 8 | 9 | if not bfx.rest.merchant.set_merchant_settings("bfx_pay_recommend_store", 1): 10 | print("Cannot set to <1>.") 11 | 12 | print( 13 | "The current value is:", 14 | bfx.rest.merchant.get_merchant_settings("bfx_pay_preferred_fiat"), 15 | ) 16 | 17 | settings = bfx.rest.merchant.list_merchant_settings( 18 | [ 19 | "bfx_pay_dust_balance_ui", 20 | "bfx_pay_merchant_customer_support_url", 21 | "bfx_pay_merchant_underpaid_threshold", 22 | ] 23 | ) 24 | 25 | for key, value in settings.items(): 26 | print(f"<{key}>:", value) 27 | -------------------------------------------------------------------------------- /examples/rest/merchant/submit_invoice.py: -------------------------------------------------------------------------------- 1 | # python -c "import examples.rest.merchant.submit_invoice" 2 | 3 | import os 4 | 5 | from bfxapi import Client 6 | from bfxapi.types import InvoiceSubmission 7 | 8 | bfx = Client(api_key=os.getenv("BFX_API_KEY"), api_secret=os.getenv("BFX_API_SECRET")) 9 | 10 | customer_info = { 11 | "nationality": "DE", 12 | "residCountry": "GB", 13 | "residCity": "London", 14 | "residZipCode": "WC2H 7NA", 15 | "residStreet": "5-6 Leicester Square", 16 | "residBuildingNo": "23 A", 17 | "fullName": "John Doe", 18 | "email": "john@example.com", 19 | } 20 | 21 | invoice: InvoiceSubmission = bfx.rest.merchant.submit_invoice( 22 | amount=1.0, 23 | currency="USD", 24 | order_id="test", 25 | customer_info=customer_info, 26 | pay_currencies=["ETH"], 27 | duration=86400, 28 | ) 29 | 30 | print("Invoice submission:", invoice) 31 | 32 | print( 33 | bfx.rest.merchant.complete_invoice(id=invoice.id, pay_currency="ETH", deposit_id=1) 34 | ) 35 | 36 | print(bfx.rest.merchant.get_invoices(limit=25)) 37 | 38 | print( 39 | bfx.rest.merchant.get_invoices_paginated( 40 | page=1, page_size=60, sort="asc", sort_field="t" 41 | ) 42 | ) 43 | -------------------------------------------------------------------------------- /examples/rest/public/book.py: -------------------------------------------------------------------------------- 1 | # python -c "import examples.rest.public.book" 2 | 3 | from typing import List 4 | 5 | from bfxapi import Client 6 | from bfxapi.types import ( 7 | FundingCurrencyBook, 8 | FundingCurrencyRawBook, 9 | TradingPairBook, 10 | TradingPairRawBook, 11 | ) 12 | 13 | bfx = Client() 14 | 15 | t_book: List[TradingPairBook] = bfx.rest.public.get_t_book( 16 | "tBTCUSD", precision="P0", len=25 17 | ) 18 | 19 | print("25 price points of tBTCUSD order book (with precision P0):", t_book) 20 | 21 | t_raw_book: List[TradingPairRawBook] = bfx.rest.public.get_t_raw_book("tBTCUSD") 22 | 23 | print("tBTCUSD raw order book:", t_raw_book) 24 | 25 | f_book: List[FundingCurrencyBook] = bfx.rest.public.get_f_book( 26 | "fUSD", precision="P0", len=25 27 | ) 28 | 29 | print("25 price points of fUSD order book (with precision P0):", f_book) 30 | 31 | f_raw_book: List[FundingCurrencyRawBook] = bfx.rest.public.get_f_raw_book("fUSD") 32 | 33 | print("fUSD raw order book:", f_raw_book) 34 | -------------------------------------------------------------------------------- /examples/rest/public/conf.py: -------------------------------------------------------------------------------- 1 | # python -c "import examples.rest.public.conf" 2 | 3 | from bfxapi import Client 4 | 5 | bfx = Client() 6 | 7 | # Prints a map from symbols to their API symbols 8 | print(bfx.rest.public.conf("pub:map:currency:sym")) 9 | 10 | # Prints all the available exchange trading pairs 11 | print(bfx.rest.public.conf("pub:list:pair:exchange")) 12 | 13 | # Prints all the available funding currencies 14 | print(bfx.rest.public.conf("pub:list:currency")) 15 | -------------------------------------------------------------------------------- /examples/rest/public/get_candles_hist.py: -------------------------------------------------------------------------------- 1 | # python -c "import examples.rest.public.get_candles_hist" 2 | 3 | from bfxapi import Client 4 | 5 | bfx = Client() 6 | 7 | print(f"Candles: {bfx.rest.public.get_candles_hist(symbol='tBTCUSD')}") 8 | 9 | # Be sure to specify a period or aggregated period when retrieving funding candles. 10 | # If you wish to mimic the candles found in the UI, use the following setup 11 | # to aggregate all funding candles: a30:p2:p30 12 | print( 13 | f"Candles: {bfx.rest.public.get_candles_hist(tf='15m', symbol='fUSD:a30:p2:p30')}" 14 | ) 15 | -------------------------------------------------------------------------------- /examples/rest/public/pulse_endpoints.py: -------------------------------------------------------------------------------- 1 | # python -c "import examples.rest.public.pulse_endpoints" 2 | 3 | import datetime 4 | from typing import List 5 | 6 | from bfxapi import Client 7 | from bfxapi.types import PulseMessage, PulseProfile 8 | 9 | bfx = Client() 10 | 11 | # POSIX timestamp in milliseconds (check https://currentmillis.com/) 12 | end = datetime.datetime(2020, 5, 2).timestamp() * 1000 13 | 14 | # Retrieves 25 pulse messages up to 2020/05/02 15 | messages: List[PulseMessage] = bfx.rest.public.get_pulse_message_history( 16 | end=end, limit=25 17 | ) 18 | 19 | for message in messages: 20 | print(f"Message author: {message.profile.nickname} ({message.profile.puid})") 21 | print(f"Title: <{message.title}>") 22 | print(f"Tags: {message.tags}\n") 23 | 24 | profile: PulseProfile = bfx.rest.public.get_pulse_profile_details("News") 25 | URL = profile.picture.replace("size", "small") 26 | print( 27 | f"<{profile.nickname}>'s profile picture:" 28 | f" https://s3-eu-west-1.amazonaws.com/bfx-pub/{URL}" 29 | ) 30 | -------------------------------------------------------------------------------- /examples/rest/public/rest_calculation_endpoints.py: -------------------------------------------------------------------------------- 1 | # python -c "import examples.rest.public.rest_calculation_endpoints" 2 | 3 | from bfxapi import Client 4 | from bfxapi.types import FundingMarketAveragePrice, FxRate, TradingMarketAveragePrice 5 | 6 | bfx = Client() 7 | 8 | trading_market_average_price: TradingMarketAveragePrice = ( 9 | bfx.rest.public.get_trading_market_average_price( 10 | symbol="tBTCUSD", amount=-100, price_limit=20000.5 11 | ) 12 | ) 13 | 14 | print("Average execution price for tBTCUSD:", trading_market_average_price.price_avg) 15 | 16 | funding_market_average_price: FundingMarketAveragePrice = ( 17 | bfx.rest.public.get_funding_market_average_price( 18 | symbol="fUSD", amount=100, period=2, rate_limit=0.00015 19 | ) 20 | ) 21 | 22 | print("Average execution rate for fUSD:", funding_market_average_price.rate_avg) 23 | 24 | fx_rate: FxRate = bfx.rest.public.get_fx_rate(ccy1="USD", ccy2="EUR") 25 | 26 | print("Exchange rate between USD and EUR:", fx_rate.current_rate) 27 | -------------------------------------------------------------------------------- /examples/rest/public/trades.py: -------------------------------------------------------------------------------- 1 | # python -c "import examples.rest.public.trades" 2 | 3 | from typing import List 4 | 5 | from bfxapi import Client 6 | from bfxapi.types import FundingCurrencyTrade, TradingPairTrade 7 | 8 | bfx = Client() 9 | 10 | t_trades: List[TradingPairTrade] = bfx.rest.public.get_t_trades( 11 | "tBTCUSD", limit=15, sort=+1 12 | ) 13 | 14 | print("Latest 15 trades for tBTCUSD (in ascending order):", t_trades) 15 | 16 | f_trades: List[FundingCurrencyTrade] = bfx.rest.public.get_f_trades( 17 | "fUSD", limit=15, sort=-1 18 | ) 19 | 20 | print("Latest 15 trades for fUSD (in descending order):", f_trades) 21 | -------------------------------------------------------------------------------- /examples/websocket/auth/calc.py: -------------------------------------------------------------------------------- 1 | # python -c "import examples.websocket.auth.calc" 2 | 3 | import os 4 | 5 | from bfxapi import Client 6 | from bfxapi.types import BaseMarginInfo, FundingInfo, SymbolMarginInfo 7 | 8 | bfx = Client( 9 | api_key=os.getenv("BFX_API_KEY"), 10 | api_secret=os.getenv("BFX_API_SECRET"), 11 | ) 12 | 13 | 14 | @bfx.wss.on("authenticated") 15 | async def on_authenticated(_): 16 | await bfx.wss.inputs.calc("margin_base", "margin_sym_tBTCUSD", "funding_sym_fUST") 17 | 18 | 19 | @bfx.wss.on("base_margin_info") 20 | def on_base_margin_info(data: BaseMarginInfo): 21 | print("Base margin info:", data) 22 | 23 | 24 | @bfx.wss.on("symbol_margin_info") 25 | def on_symbol_margin_info(data: SymbolMarginInfo): 26 | if data.symbol == "tBTCUSD": 27 | print("Symbol margin info:", data) 28 | 29 | 30 | @bfx.wss.on("funding_info_update") 31 | def on_funding_info_update(data: FundingInfo): 32 | if data.symbol == "fUST": 33 | print("Funding info update:", data) 34 | 35 | 36 | bfx.wss.run() 37 | -------------------------------------------------------------------------------- /examples/websocket/auth/submit_order.py: -------------------------------------------------------------------------------- 1 | # python -c "import examples.websocket.auth.submit_order" 2 | 3 | import os 4 | 5 | from bfxapi import Client 6 | from bfxapi.types import Notification, Order 7 | 8 | bfx = Client( 9 | api_key=os.getenv("BFX_API_KEY"), 10 | api_secret=os.getenv("BFX_API_SECRET"), 11 | ) 12 | 13 | 14 | @bfx.wss.on("authenticated") 15 | async def on_authenticated(event): 16 | print(f"Authentication: {event}") 17 | 18 | await bfx.wss.inputs.submit_order( 19 | type="EXCHANGE LIMIT", symbol="tBTCUSD", amount=0.165212, price=30264.0 20 | ) 21 | 22 | print("The order has been sent.") 23 | 24 | 25 | @bfx.wss.on("on-req-notification") 26 | async def on_notification(notification: Notification[Order]): 27 | print(f"Notification: {notification}.") 28 | 29 | 30 | @bfx.wss.on("order_new") 31 | async def on_order_new(order_new: Order): 32 | print(f"Order new: {order_new}") 33 | 34 | 35 | @bfx.wss.on("subscribed") 36 | def on_subscribed(subscription): 37 | print(f"Subscription successful for <{subscription}>.") 38 | 39 | 40 | bfx.wss.run() 41 | -------------------------------------------------------------------------------- /examples/websocket/auth/wallets.py: -------------------------------------------------------------------------------- 1 | # python -c "import examples.websocket.auth.wallets" 2 | 3 | import os 4 | from typing import List 5 | 6 | from bfxapi import Client 7 | from bfxapi.types import Wallet 8 | 9 | bfx = Client( 10 | api_key=os.getenv("BFX_API_KEY"), 11 | api_secret=os.getenv("BFX_API_SECRET"), 12 | filters=["wallet"], 13 | ) 14 | 15 | 16 | @bfx.wss.on("wallet_snapshot") 17 | def on_wallet_snapshot(wallets: List[Wallet]): 18 | for wallet in wallets: 19 | print(f"Wallet: {wallet.wallet_type} | {wallet.currency}") 20 | print(f"Available balance: {wallet.available_balance}") 21 | print(f"Wallet trade details: {wallet.trade_details}") 22 | 23 | 24 | @bfx.wss.on("wallet_update") 25 | def on_wallet_update(wallet: Wallet): 26 | print(f"Wallet update: {wallet}") 27 | 28 | 29 | bfx.wss.run() 30 | -------------------------------------------------------------------------------- /examples/websocket/public/derivatives_status.py: -------------------------------------------------------------------------------- 1 | # python -c "import examples.websocket.public.derivatives_status" 2 | 3 | from bfxapi import Client 4 | from bfxapi.types import DerivativesStatus 5 | from bfxapi.websocket.subscriptions import Status 6 | 7 | bfx = Client() 8 | 9 | 10 | @bfx.wss.on("derivatives_status_update") 11 | def on_derivatives_status_update(subscription: Status, data: DerivativesStatus): 12 | print(f"{subscription}:", data) 13 | 14 | 15 | @bfx.wss.on("open") 16 | async def on_open(): 17 | await bfx.wss.subscribe("status", key="deriv:tBTCF0:USTF0") 18 | 19 | 20 | bfx.wss.run() 21 | -------------------------------------------------------------------------------- /examples/websocket/public/order_book.py: -------------------------------------------------------------------------------- 1 | # python -c "import examples.websocket.public.order_book" 2 | 3 | import zlib 4 | from collections import OrderedDict 5 | from decimal import Decimal 6 | from math import floor, log10 7 | from typing import Any, Dict, List, cast 8 | 9 | from bfxapi import Client 10 | from bfxapi.types import TradingPairBook 11 | from bfxapi.websocket.subscriptions import Book 12 | 13 | 14 | def _format_float(value: float) -> str: 15 | """ 16 | Format float numbers into a string compatible with the Bitfinex API. 17 | """ 18 | 19 | def _find_exp(number: float) -> int: 20 | base10 = log10(abs(number)) 21 | 22 | return floor(base10) 23 | 24 | if _find_exp(value) >= -6: 25 | return format(Decimal(repr(value)), "f") 26 | 27 | return str(value).replace("e-0", "e-") 28 | 29 | 30 | class OrderBook: 31 | def __init__(self, symbols: List[str]): 32 | self.__order_book = { 33 | symbol: {"bids": OrderedDict(), "asks": OrderedDict()} for symbol in symbols 34 | } 35 | 36 | def update(self, symbol: str, data: TradingPairBook) -> None: 37 | price, count, amount = data.price, data.count, data.amount 38 | 39 | kind = "bids" if amount > 0 else "asks" 40 | 41 | if count > 0: 42 | self.__order_book[symbol][kind][price] = { 43 | "price": price, 44 | "count": count, 45 | "amount": amount, 46 | } 47 | 48 | if count == 0: 49 | if price in self.__order_book[symbol][kind]: 50 | del self.__order_book[symbol][kind][price] 51 | 52 | def verify(self, symbol: str, checksum: int) -> bool: 53 | values: List[int] = [] 54 | 55 | bids = sorted( 56 | [ 57 | (data["price"], data["count"], data["amount"]) 58 | for _, data in self.__order_book[symbol]["bids"].items() 59 | ], 60 | key=lambda data: -data[0], 61 | ) 62 | 63 | asks = sorted( 64 | [ 65 | (data["price"], data["count"], data["amount"]) 66 | for _, data in self.__order_book[symbol]["asks"].items() 67 | ], 68 | key=lambda data: data[0], 69 | ) 70 | 71 | if len(bids) < 25 or len(asks) < 25: 72 | raise AssertionError("Not enough bids or asks (need at least 25).") 73 | 74 | for _i in range(25): 75 | bid, ask = bids[_i], asks[_i] 76 | values.extend([bid[0], bid[2]]) 77 | values.extend([ask[0], ask[2]]) 78 | 79 | local = ":".join(_format_float(value) for value in values) 80 | 81 | crc32 = zlib.crc32(local.encode("UTF-8")) 82 | 83 | return crc32 == checksum 84 | 85 | def is_verifiable(self, symbol: str) -> bool: 86 | return ( 87 | len(self.__order_book[symbol]["bids"]) >= 25 88 | and len(self.__order_book[symbol]["asks"]) >= 25 89 | ) 90 | 91 | def clear(self, symbol: str) -> None: 92 | self.__order_book[symbol] = {"bids": OrderedDict(), "asks": OrderedDict()} 93 | 94 | 95 | SYMBOLS = ["tLTCBTC", "tETHUSD", "tETHBTC"] 96 | 97 | order_book = OrderBook(symbols=SYMBOLS) 98 | 99 | bfx = Client() 100 | 101 | 102 | @bfx.wss.on("open") 103 | async def on_open(): 104 | for symbol in SYMBOLS: 105 | await bfx.wss.subscribe("book", symbol=symbol) 106 | 107 | 108 | @bfx.wss.on("subscribed") 109 | def on_subscribed(subscription): 110 | print(f"Subscription successful for symbol <{subscription['symbol']}>") 111 | 112 | 113 | @bfx.wss.on("t_book_snapshot") 114 | def on_t_book_snapshot(subscription: Book, snapshot: List[TradingPairBook]): 115 | for data in snapshot: 116 | order_book.update(subscription["symbol"], data) 117 | 118 | 119 | @bfx.wss.on("t_book_update") 120 | def on_t_book_update(subscription: Book, data: TradingPairBook): 121 | order_book.update(subscription["symbol"], data) 122 | 123 | 124 | @bfx.wss.on("checksum") 125 | async def on_checksum(subscription: Book, value: int): 126 | symbol = subscription["symbol"] 127 | 128 | if order_book.is_verifiable(symbol): 129 | if not order_book.verify(symbol, value): 130 | print( 131 | "Mismatch between local and remote checksums: " 132 | + f"restarting book for symbol <{symbol}>..." 133 | ) 134 | 135 | _subscription = cast(Dict[str, Any], subscription.copy()) 136 | 137 | await bfx.wss.unsubscribe(sub_id=_subscription.pop("sub_id")) 138 | 139 | await bfx.wss.subscribe(**_subscription) 140 | 141 | order_book.clear(symbol) 142 | 143 | 144 | bfx.wss.run() 145 | -------------------------------------------------------------------------------- /examples/websocket/public/raw_order_book.py: -------------------------------------------------------------------------------- 1 | # python -c "import examples.websocket.public.raw_order_book" 2 | 3 | import zlib 4 | from collections import OrderedDict 5 | from decimal import Decimal 6 | from math import floor, log10 7 | from typing import Any, Dict, List, cast 8 | 9 | from bfxapi import Client 10 | from bfxapi.types import TradingPairRawBook 11 | from bfxapi.websocket.subscriptions import Book 12 | 13 | 14 | def _format_float(value: float) -> str: 15 | """ 16 | Format float numbers into a string compatible with the Bitfinex API. 17 | """ 18 | 19 | def _find_exp(number: float) -> int: 20 | base10 = log10(abs(number)) 21 | 22 | return floor(base10) 23 | 24 | if _find_exp(value) >= -6: 25 | return format(Decimal(repr(value)), "f") 26 | 27 | return str(value).replace("e-0", "e-") 28 | 29 | 30 | class RawOrderBook: 31 | def __init__(self, symbols: List[str]): 32 | self.__raw_order_book = { 33 | symbol: {"bids": OrderedDict(), "asks": OrderedDict()} for symbol in symbols 34 | } 35 | 36 | def update(self, symbol: str, data: TradingPairRawBook) -> None: 37 | order_id, price, amount = data.order_id, data.price, data.amount 38 | 39 | kind = "bids" if amount > 0 else "asks" 40 | 41 | if price > 0: 42 | self.__raw_order_book[symbol][kind][order_id] = { 43 | "order_id": order_id, 44 | "price": price, 45 | "amount": amount, 46 | } 47 | 48 | if price == 0: 49 | if order_id in self.__raw_order_book[symbol][kind]: 50 | del self.__raw_order_book[symbol][kind][order_id] 51 | 52 | def verify(self, symbol: str, checksum: int) -> bool: 53 | values: List[int] = [] 54 | 55 | bids = sorted( 56 | [ 57 | (data["order_id"], data["price"], data["amount"]) 58 | for _, data in self.__raw_order_book[symbol]["bids"].items() 59 | ], 60 | key=lambda data: (-data[1], data[0]), 61 | ) 62 | 63 | asks = sorted( 64 | [ 65 | (data["order_id"], data["price"], data["amount"]) 66 | for _, data in self.__raw_order_book[symbol]["asks"].items() 67 | ], 68 | key=lambda data: (data[1], data[0]), 69 | ) 70 | 71 | if len(bids) < 25 or len(asks) < 25: 72 | raise AssertionError("Not enough bids or asks (need at least 25).") 73 | 74 | for _i in range(25): 75 | bid, ask = bids[_i], asks[_i] 76 | values.extend([bid[0], bid[2]]) 77 | values.extend([ask[0], ask[2]]) 78 | 79 | local = ":".join(_format_float(value) for value in values) 80 | 81 | crc32 = zlib.crc32(local.encode("UTF-8")) 82 | 83 | return crc32 == checksum 84 | 85 | def is_verifiable(self, symbol: str) -> bool: 86 | return ( 87 | len(self.__raw_order_book[symbol]["bids"]) >= 25 88 | and len(self.__raw_order_book[symbol]["asks"]) >= 25 89 | ) 90 | 91 | def clear(self, symbol: str) -> None: 92 | self.__raw_order_book[symbol] = {"bids": OrderedDict(), "asks": OrderedDict()} 93 | 94 | 95 | SYMBOLS = ["tLTCBTC", "tETHUSD", "tETHBTC"] 96 | 97 | raw_order_book = RawOrderBook(symbols=SYMBOLS) 98 | 99 | bfx = Client() 100 | 101 | 102 | @bfx.wss.on("open") 103 | async def on_open(): 104 | for symbol in SYMBOLS: 105 | await bfx.wss.subscribe("book", symbol=symbol, prec="R0") 106 | 107 | 108 | @bfx.wss.on("subscribed") 109 | def on_subscribed(subscription): 110 | print(f"Subscription successful for symbol <{subscription['symbol']}>") 111 | 112 | 113 | @bfx.wss.on("t_raw_book_snapshot") 114 | def on_t_raw_book_snapshot(subscription: Book, snapshot: List[TradingPairRawBook]): 115 | for data in snapshot: 116 | raw_order_book.update(subscription["symbol"], data) 117 | 118 | 119 | @bfx.wss.on("t_raw_book_update") 120 | def on_t_raw_book_update(subscription: Book, data: TradingPairRawBook): 121 | raw_order_book.update(subscription["symbol"], data) 122 | 123 | 124 | @bfx.wss.on("checksum") 125 | async def on_checksum(subscription: Book, value: int): 126 | symbol = subscription["symbol"] 127 | 128 | if raw_order_book.is_verifiable(symbol): 129 | if not raw_order_book.verify(symbol, value): 130 | print( 131 | "Mismatch between local and remote checksums: " 132 | + f"restarting book for symbol <{symbol}>..." 133 | ) 134 | 135 | _subscription = cast(Dict[str, Any], subscription.copy()) 136 | 137 | await bfx.wss.unsubscribe(sub_id=_subscription.pop("sub_id")) 138 | 139 | await bfx.wss.subscribe(**_subscription) 140 | 141 | raw_order_book.clear(symbol) 142 | 143 | 144 | bfx.wss.run() 145 | -------------------------------------------------------------------------------- /examples/websocket/public/ticker.py: -------------------------------------------------------------------------------- 1 | # python -c "import examples.websocket.public.ticker" 2 | 3 | from bfxapi import Client 4 | from bfxapi.types import TradingPairTicker 5 | from bfxapi.websocket.subscriptions import Ticker 6 | 7 | bfx = Client() 8 | 9 | 10 | @bfx.wss.on("t_ticker_update") 11 | def on_t_ticker_update(subscription: Ticker, data: TradingPairTicker): 12 | print(f"Subscription with sub_id: {subscription['sub_id']}") 13 | 14 | print(f"Data: {data}") 15 | 16 | 17 | @bfx.wss.on("open") 18 | async def on_open(): 19 | await bfx.wss.subscribe("ticker", symbol="tBTCUSD") 20 | 21 | 22 | bfx.wss.run() 23 | -------------------------------------------------------------------------------- /examples/websocket/public/trades.py: -------------------------------------------------------------------------------- 1 | # python -c "import examples.websocket.public.trades" 2 | 3 | from bfxapi import Client 4 | from bfxapi.types import Candle, TradingPairTrade 5 | from bfxapi.websocket.subscriptions import Candles, Trades 6 | 7 | bfx = Client() 8 | 9 | 10 | @bfx.wss.on("candles_update") 11 | def on_candles_update(_sub: Candles, candle: Candle): 12 | print(f"New candle: {candle}") 13 | 14 | 15 | @bfx.wss.on("t_trade_execution") 16 | def on_t_trade_execution(_sub: Trades, trade: TradingPairTrade): 17 | print(f"New trade: {trade}") 18 | 19 | 20 | @bfx.wss.on("open") 21 | async def on_open(): 22 | await bfx.wss.subscribe("candles", key="trade:1m:tBTCUSD") 23 | 24 | await bfx.wss.subscribe("trades", symbol="tBTCUSD") 25 | 26 | 27 | bfx.wss.run() 28 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | target-version = ["py38", "py39", "py310", "py311"] 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitfinexcom/bitfinex-api-py/791c84591f354914784643eed8fd1ac92c98c536/requirements.txt -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | 3 | setup( 4 | name="bitfinex-api-py", 5 | version="3.0.5", 6 | description="Official Bitfinex Python API", 7 | long_description=( 8 | "A Python reference implementation of the Bitfinex API " 9 | "for both REST and websocket interaction." 10 | ), 11 | long_description_content_type="text/markdown", 12 | url="https://github.com/bitfinexcom/bitfinex-api-py", 13 | author="Bitfinex", 14 | author_email="support@bitfinex.com", 15 | license="Apache-2.0", 16 | classifiers=[ 17 | "Development Status :: 5 - Production/Stable", 18 | "Intended Audience :: Developers", 19 | "Topic :: Software Development :: Build Tools", 20 | "License :: OSI Approved :: Apache Software License", 21 | "Programming Language :: Python :: 3.8", 22 | "Programming Language :: Python :: 3.9", 23 | "Programming Language :: Python :: 3.10", 24 | "Programming Language :: Python :: 3.11", 25 | "Programming Language :: Python :: 3.12", 26 | "Programming Language :: Python :: 3.13", 27 | ], 28 | keywords="bitfinex,api,trading", 29 | project_urls={ 30 | "Bug Reports": "https://github.com/bitfinexcom/bitfinex-api-py/issues", 31 | "Source": "https://github.com/bitfinexcom/bitfinex-api-py", 32 | }, 33 | packages=[ 34 | "bfxapi", 35 | "bfxapi._utils", 36 | "bfxapi.types", 37 | "bfxapi.websocket", 38 | "bfxapi.websocket._client", 39 | "bfxapi.websocket._handlers", 40 | "bfxapi.websocket._event_emitter", 41 | "bfxapi.rest", 42 | "bfxapi.rest._interface", 43 | "bfxapi.rest._interfaces", 44 | ], 45 | install_requires=[ 46 | "pyee~=11.1.0", 47 | "websockets~=12.0", 48 | "requests~=2.32.3", 49 | ], 50 | extras_require={ 51 | "typing": [ 52 | "types-requests~=2.32.0.20241016", 53 | ] 54 | }, 55 | python_requires=">=3.8", 56 | package_data={"bfxapi": ["py.typed"]}, 57 | ) 58 | --------------------------------------------------------------------------------