Repository: crypto-crawler/crypto-crawler-rs Branch: main Commit: 8091c2ccc932 Files: 306 Total size: 926.4 KB Directory structure: gitextract_sqwwf4gd/ ├── .config/ │ └── nextest.toml ├── .editorconfig ├── .github/ │ ├── CODEOWNERS │ └── workflows/ │ └── ci.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── crypto-client/ │ ├── Cargo.toml │ ├── README.md │ └── src/ │ └── lib.rs ├── crypto-crawler/ │ ├── Cargo.toml │ ├── README.md │ ├── src/ │ │ ├── crawlers/ │ │ │ ├── binance.rs │ │ │ ├── bitmex.rs │ │ │ ├── deribit.rs │ │ │ ├── huobi.rs │ │ │ ├── kucoin.rs │ │ │ ├── mod.rs │ │ │ ├── okx.rs │ │ │ ├── utils.rs │ │ │ ├── zb.rs │ │ │ └── zbg.rs │ │ ├── lib.rs │ │ ├── msg.rs │ │ └── utils/ │ │ ├── cmc_rank.rs │ │ ├── lock.rs │ │ ├── mod.rs │ │ └── spot_symbols.rs │ └── tests/ │ ├── binance.rs │ ├── bitfinex.rs │ ├── bitget.rs │ ├── bithumb.rs │ ├── bitmex.rs │ ├── bitstamp.rs │ ├── bitz.rs │ ├── bybit.rs │ ├── coinbase_pro.rs │ ├── deribit.rs │ ├── dydx.rs │ ├── ftx.rs │ ├── gate.rs │ ├── huobi.rs │ ├── kraken.rs │ ├── kucoin.rs │ ├── mexc.rs │ ├── okx.rs │ ├── utils/ │ │ └── mod.rs │ ├── zb.rs │ └── zbg.rs ├── crypto-market-type/ │ ├── Cargo.toml │ ├── include/ │ │ └── crypto_market_type.h │ └── src/ │ └── lib.rs ├── crypto-markets/ │ ├── Cargo.toml │ ├── README.md │ ├── src/ │ │ ├── error.rs │ │ ├── exchanges/ │ │ │ ├── binance/ │ │ │ │ ├── binance_inverse.rs │ │ │ │ ├── binance_linear.rs │ │ │ │ ├── binance_option.rs │ │ │ │ ├── binance_spot.rs │ │ │ │ ├── mod.rs │ │ │ │ └── utils.rs │ │ │ ├── bitfinex.rs │ │ │ ├── bitget/ │ │ │ │ ├── bitget_spot.rs │ │ │ │ ├── bitget_swap.rs │ │ │ │ └── mod.rs │ │ │ ├── bithumb.rs │ │ │ ├── bitmex.rs │ │ │ ├── bitstamp.rs │ │ │ ├── bitz/ │ │ │ │ ├── bitz_spot.rs │ │ │ │ ├── bitz_swap.rs │ │ │ │ └── mod.rs │ │ │ ├── bybit.rs │ │ │ ├── coinbase_pro.rs │ │ │ ├── deribit/ │ │ │ │ ├── mod.rs │ │ │ │ └── utils.rs │ │ │ ├── dydx/ │ │ │ │ ├── dydx_swap.rs │ │ │ │ └── mod.rs │ │ │ ├── ftx.rs │ │ │ ├── gate/ │ │ │ │ ├── gate_future.rs │ │ │ │ ├── gate_spot.rs │ │ │ │ ├── gate_swap.rs │ │ │ │ └── mod.rs │ │ │ ├── huobi/ │ │ │ │ ├── huobi_future.rs │ │ │ │ ├── huobi_inverse_swap.rs │ │ │ │ ├── huobi_linear_swap.rs │ │ │ │ ├── huobi_option.rs │ │ │ │ ├── huobi_spot.rs │ │ │ │ ├── mod.rs │ │ │ │ └── utils.rs │ │ │ ├── kraken/ │ │ │ │ ├── kraken_futures.rs │ │ │ │ ├── kraken_spot.rs │ │ │ │ └── mod.rs │ │ │ ├── kucoin/ │ │ │ │ ├── kucoin_spot.rs │ │ │ │ ├── kucoin_swap.rs │ │ │ │ └── mod.rs │ │ │ ├── mexc/ │ │ │ │ ├── mexc_spot.rs │ │ │ │ ├── mexc_swap.rs │ │ │ │ ├── mod.rs │ │ │ │ └── utils.rs │ │ │ ├── mod.rs │ │ │ ├── okx.rs │ │ │ ├── utils.rs │ │ │ ├── zb/ │ │ │ │ ├── mod.rs │ │ │ │ ├── zb_spot.rs │ │ │ │ └── zb_swap.rs │ │ │ └── zbg/ │ │ │ ├── mod.rs │ │ │ ├── zbg_spot.rs │ │ │ └── zbg_swap.rs │ │ ├── lib.rs │ │ ├── main.rs │ │ └── market.rs │ └── tests/ │ ├── binance.rs │ ├── bitfinex.rs │ ├── bitget.rs │ ├── bithumb.rs │ ├── bitmex.rs │ ├── bitstamp.rs │ ├── bitz.rs │ ├── bybit.rs │ ├── coinbase_pro.rs │ ├── deribit.rs │ ├── dydx.rs │ ├── ftx.rs │ ├── gate.rs │ ├── huobi.rs │ ├── kraken.rs │ ├── kucoin.rs │ ├── mexc.rs │ ├── okx.rs │ ├── utils/ │ │ └── mod.rs │ ├── zb.rs │ └── zbg.rs ├── crypto-msg-type/ │ ├── Cargo.toml │ ├── include/ │ │ └── crypto_msg_type.h │ └── src/ │ ├── exchanges/ │ │ ├── binance.rs │ │ ├── bitfinex.rs │ │ ├── bitmex.rs │ │ ├── bybit.rs │ │ ├── deribit.rs │ │ ├── ftx.rs │ │ ├── huobi.rs │ │ ├── mod.rs │ │ ├── okex.rs │ │ └── okx.rs │ └── lib.rs ├── crypto-rest-client/ │ ├── Cargo.toml │ ├── README.md │ ├── src/ │ │ ├── error.rs │ │ ├── exchanges/ │ │ │ ├── binance/ │ │ │ │ ├── binance_inverse.rs │ │ │ │ ├── binance_linear.rs │ │ │ │ ├── binance_option.rs │ │ │ │ ├── binance_spot.rs │ │ │ │ ├── mod.rs │ │ │ │ └── utils.rs │ │ │ ├── bitfinex.rs │ │ │ ├── bitget/ │ │ │ │ ├── bitget_spot.rs │ │ │ │ ├── bitget_swap.rs │ │ │ │ └── mod.rs │ │ │ ├── bithumb.rs │ │ │ ├── bitmex.rs │ │ │ ├── bitstamp.rs │ │ │ ├── bitz/ │ │ │ │ ├── bitz_spot.rs │ │ │ │ ├── bitz_swap.rs │ │ │ │ └── mod.rs │ │ │ ├── bybit.rs │ │ │ ├── coinbase_pro.rs │ │ │ ├── deribit.rs │ │ │ ├── dydx/ │ │ │ │ ├── dydx_swap.rs │ │ │ │ └── mod.rs │ │ │ ├── ftx.rs │ │ │ ├── gate/ │ │ │ │ ├── gate_future.rs │ │ │ │ ├── gate_spot.rs │ │ │ │ ├── gate_swap.rs │ │ │ │ └── mod.rs │ │ │ ├── huobi/ │ │ │ │ ├── huobi_future.rs │ │ │ │ ├── huobi_inverse_swap.rs │ │ │ │ ├── huobi_linear_swap.rs │ │ │ │ ├── huobi_option.rs │ │ │ │ ├── huobi_spot.rs │ │ │ │ ├── mod.rs │ │ │ │ └── utils.rs │ │ │ ├── kraken/ │ │ │ │ ├── kraken_futures.rs │ │ │ │ ├── kraken_spot.rs │ │ │ │ └── mod.rs │ │ │ ├── kucoin/ │ │ │ │ ├── kucoin_spot.rs │ │ │ │ ├── kucoin_swap.rs │ │ │ │ └── mod.rs │ │ │ ├── mexc/ │ │ │ │ ├── mexc_spot.rs │ │ │ │ ├── mexc_swap.rs │ │ │ │ └── mod.rs │ │ │ ├── mod.rs │ │ │ ├── okx.rs │ │ │ ├── utils.rs │ │ │ ├── zb/ │ │ │ │ ├── mod.rs │ │ │ │ ├── zb_spot.rs │ │ │ │ └── zb_swap.rs │ │ │ └── zbg/ │ │ │ ├── mod.rs │ │ │ ├── zbg_spot.rs │ │ │ └── zbg_swap.rs │ │ └── lib.rs │ └── tests/ │ ├── binance_inverse.rs │ ├── binance_linear.rs │ ├── binance_option.rs │ ├── binance_spot.rs │ ├── bitfinex.rs │ ├── bitget_spot.rs │ ├── bitget_swap.rs │ ├── bithumb.rs │ ├── bitmex.rs │ ├── bitstamp.rs │ ├── bitz_spot.rs │ ├── bitz_swap.rs │ ├── bybit.rs │ ├── coinbase_pro.rs │ ├── deribit.rs │ ├── dydx.rs │ ├── ftx.rs │ ├── gate.rs │ ├── huobi.rs │ ├── kraken.rs │ ├── kucoin.rs │ ├── mexc.rs │ ├── okx.rs │ ├── zb.rs │ └── zbg.rs ├── crypto-ws-client/ │ ├── Cargo.toml │ ├── README.md │ ├── src/ │ │ ├── clients/ │ │ │ ├── binance.rs │ │ │ ├── binance_option.rs │ │ │ ├── bitfinex.rs │ │ │ ├── bitget/ │ │ │ │ ├── bitget_spot.rs │ │ │ │ ├── bitget_swap.rs │ │ │ │ ├── mod.rs │ │ │ │ └── utils.rs │ │ │ ├── bithumb.rs │ │ │ ├── bitmex.rs │ │ │ ├── bitstamp.rs │ │ │ ├── bitz/ │ │ │ │ ├── bitz_spot.rs │ │ │ │ └── mod.rs │ │ │ ├── bybit/ │ │ │ │ ├── bybit_inverse.rs │ │ │ │ ├── bybit_linear_swap.rs │ │ │ │ ├── mod.rs │ │ │ │ └── utils.rs │ │ │ ├── coinbase_pro.rs │ │ │ ├── common_traits.rs │ │ │ ├── deribit.rs │ │ │ ├── dydx/ │ │ │ │ ├── dydx_swap.rs │ │ │ │ └── mod.rs │ │ │ ├── ftx.rs │ │ │ ├── gate/ │ │ │ │ ├── gate_future.rs │ │ │ │ ├── gate_spot.rs │ │ │ │ ├── gate_swap.rs │ │ │ │ ├── mod.rs │ │ │ │ └── utils.rs │ │ │ ├── huobi.rs │ │ │ ├── kraken/ │ │ │ │ ├── kraken_futures.rs │ │ │ │ ├── kraken_spot.rs │ │ │ │ └── mod.rs │ │ │ ├── kucoin/ │ │ │ │ ├── kucoin_spot.rs │ │ │ │ ├── kucoin_swap.rs │ │ │ │ ├── mod.rs │ │ │ │ └── utils.rs │ │ │ ├── mexc/ │ │ │ │ ├── mexc_spot.rs │ │ │ │ ├── mexc_swap.rs │ │ │ │ └── mod.rs │ │ │ ├── mod.rs │ │ │ ├── okx.rs │ │ │ ├── zb/ │ │ │ │ ├── mod.rs │ │ │ │ ├── zb_spot.rs │ │ │ │ └── zb_swap.rs │ │ │ └── zbg/ │ │ │ ├── mod.rs │ │ │ ├── utils.rs │ │ │ ├── zbg_spot.rs │ │ │ └── zbg_swap.rs │ │ ├── common/ │ │ │ ├── command_translator.rs │ │ │ ├── connect_async.rs │ │ │ ├── message_handler.rs │ │ │ ├── mod.rs │ │ │ ├── utils.rs │ │ │ ├── ws_client.rs │ │ │ └── ws_client_internal.rs │ │ └── lib.rs │ └── tests/ │ ├── binance.rs │ ├── binance_option.rs │ ├── bitfinex.rs │ ├── bitget.rs │ ├── bithumb.rs │ ├── bitmex.rs │ ├── bitstamp.rs │ ├── bitz.rs │ ├── bybit.rs │ ├── coinbase_pro.rs │ ├── deribit.rs │ ├── dydx.rs │ ├── ftx.rs │ ├── gate.rs │ ├── huobi.rs │ ├── kraken.rs │ ├── kucoin.rs │ ├── mexc.rs │ ├── okx.rs │ ├── utils/ │ │ └── mod.rs │ ├── zb.rs │ └── zbg.rs └── rustfmt.toml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .config/nextest.toml ================================================ [profile.default] slow-timeout = { period = "60s", terminate-after = 2 } fail-fast = false retries = 1 ================================================ FILE: .editorconfig ================================================ # https://EditorConfig.org root = true [*] charset = utf-8 end_of_line = lf indent_size = 4 indent_style = space insert_final_newline = true trim_trailing_whitespace = true # Markdown Files [*.md] trim_trailing_whitespace = false # Batch Files [*.{cmd,bat}] end_of_line = crlf ================================================ FILE: .github/CODEOWNERS ================================================ * @soulmachine ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: [push, pull_request] env: CARGO_TERM_COLOR: always # Stop the previous CI tasks (which is deprecated) # to conserve the runner resource. concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: build: name: Cargo build runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: toolchain: nightly override: true - uses: actions-rs/cargo@v1 with: command: build args: --release --all-features test: name: Cargo test runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: toolchain: nightly override: true - uses: taiki-e/install-action@nextest - uses: actions-rs/cargo@v1 with: command: build - name: Run cargo nextest run: | # Run all tests except: # * bitmex, because bitmex has very low rate limit # * FTX, the FTX website is not operational # * zbg, due to the "invalid peer certificate: UnknownIssuer" error cargo nextest run -E 'all() - binary(~bitmex) - binary(~ftx) - binary(~zbg)' # Run the '*bitmex*' tests in -j1. cargo nextest run -E 'binary(~bitmex)' -j1 || true doc-test: name: Cargo doctest runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: toolchain: nightly override: true - uses: actions-rs/cargo@v1 with: command: test args: --doc fmt: name: Cargo fmt runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: toolchain: nightly override: true components: rustfmt - uses: actions-rs/cargo@v1 with: command: fmt args: -- --check check: name: Cargo check runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: toolchain: nightly override: true - uses: actions-rs/cargo@v1 with: command: check clippy: name: Cargo clippy runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: toolchain: nightly override: true components: clippy - uses: actions-rs/cargo@v1 with: command: clippy ================================================ FILE: .gitignore ================================================ # Generated by Cargo # will have compiled files and executables /target/ # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk # IDEA configurations /.idea *.iml *.project *.classpath ================================================ FILE: Cargo.toml ================================================ [workspace] members = [ "crypto-client", "crypto-crawler", "crypto-markets", "crypto-market-type", "crypto-msg-type", "crypto-rest-client", "crypto-ws-client", ] ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ # crypto-crawler-rs [![](https://img.shields.io/github/workflow/status/crypto-crawler/crypto-crawler-rs/CI/main)](https://github.com/crypto-crawler/crypto-crawler-rs/actions?query=branch%3Amain) [![](https://img.shields.io/crates/v/crypto-crawler.svg)](https://crates.io/crates/crypto-crawler) [![](https://docs.rs/crypto-crawler/badge.svg)](https://docs.rs/crypto-crawler) [![Discord](https://img.shields.io/discord/1043987684164649020?logo=discord)](https://discord.gg/Vych8DNZU2) ========== A rock-solid cryprocurrency crawler. ## Quickstart Use the [carbonbot](https://github.com/crypto-crawler/carbonbot) binary to crawl data. If you need more control and customization, use this library. ## Architecture ![](./dependency-tree.svg) - [crypto-crawler](./crypto-crawler) is the crawler library to crawl websocket and restful messages from exchanges - [carbonbot](https://github.com/crypto-crawler/carbonbot) is the main CLI tool to run crawlers. - [crypto-ws-client](./crypto-ws-client) is the underlying websocket client library, providing a set of universal APIs for different exchanges. - [crypto-rest-client](./crypto-rest-client) is the underlying RESTful client library, providing universal APIs to get public data from different exchanges. - [crypto-markets](./crypto-markets) is a RESTful library to retreive market meta data from cryptocurrency echanges. - [crypto-client](./crypto-client) is a RESTful client library to place and cancel orders. - Support multiple languages. Some libraries support multiple languages, which is achieved by first providing a FFI binding, then a languge specific wrapper. For example, `crypto-crawler` provides a C-style FFI binding first, and then provides a Python wrapper and a C++ wrapper based on the FFI binding. ## How to parse raw messages Use the [crypto-msg-parser](https://github.com/crypto-crawler/crypto-msg-parser) library to parse raw messages. Crawlers should always preserve the original data without any parsing. ================================================ FILE: crypto-client/Cargo.toml ================================================ [package] name = "crypto-client" version = "0.1.0" authors = ["soulmachine "] edition = "2021" description = "An unified restful client for all cryptocurrency exchanges." license = "Apache-2.0" repository = "https://github.com/crypto-crawler/crypto-crawler-rs/tree/main/crypto-client" homepage = "https://github.com/crypto-crawler/crypto-crawler-rs/tree/main/crypto-client" [dependencies] reqwest = { version = "0.11.11", features = ["blocking", "json"] } serde = { version = "1.0.140", features = ["derive"] } serde_json = "1.0.82" ================================================ FILE: crypto-client/README.md ================================================ # crypto-client An unified client for all cryptocurrency exchanges. ## Example ```rust use crypto_client::{CryptoClient, MarketType}; fn main() { let config: HashMap<&str, &str> = vec![ ("eosAccount", "your-eos-account"), ("eosPrivateKey", "your-eos-private-key"), ].into_iter().collect(); let crypto_client = CryptoClient::new(config); // buy let transaction_id = crypto_client.place_order( { exchange: "Newdex", pair: "EIDOS_EOS", market_type: "Spot" }, 0.00121, 9.2644, false, ); println!("{}", transactionId); } ``` ## Supported Exchanges - Binance - Huobi - OKEx - WhaleEx ================================================ FILE: crypto-client/src/lib.rs ================================================ /// An unified restful client for all cryptocurrency exchanges. pub struct CryptoClient { #[allow(dead_code)] exchange: String, } #[cfg(test)] mod tests { #[test] fn it_works() { assert_eq!(2 + 2, 4); } } ================================================ FILE: crypto-crawler/Cargo.toml ================================================ [package] name = "crypto-crawler" version = "4.7.9" authors = ["soulmachine "] edition = "2021" description = "A rock-solid cryprocurrency crawler." license = "Apache-2.0" repository = "https://github.com/crypto-crawler/crypto-crawler-rs/tree/main/crypto-crawler" keywords = ["cryptocurrency", "blockchain", "trading"] [dependencies] crypto-markets = "1.3.11" crypto-market-type = "1.1.5" crypto-msg-parser = "2.8.26" crypto-msg-type = "1.0.11" crypto-pair = "2.3.13" crypto-rest-client = "1.0.1" crypto-ws-client = "4.12.11" fslock = "0.2.1" once_cell = "1.17.1" log = "0.4.17" rand = "0.8.5" reqwest = { version = "0.11.14", features = ["blocking", "gzip"] } serde = { version = "1.0.157", features = ["derive"] } serde_json = "1.0.94" tokio = { version = "1.26.0", features = ["macros", "rt-multi-thread", "sync", "time"] } [dev_dependencies] env_logger = "0.9" test-case = "1" tokio = { version = "1", features = ["test-util"] } ================================================ FILE: crypto-crawler/README.md ================================================ # crypto-crawler [![](https://img.shields.io/github/workflow/status/crypto-crawler/crypto-crawler-rs/CI/main)](https://github.com/crypto-crawler/crypto-crawler-rs/actions?query=branch%3Amain) [![](https://img.shields.io/crates/v/crypto-crawler.svg)](https://crates.io/crates/crypto-crawler) [![](https://docs.rs/crypto-crawler/badge.svg)](https://docs.rs/crypto-crawler) ========== A rock-solid cryprocurrency crawler. ## Crawl realtime trades ```rust use crypto_crawler::{crawl_trade, MarketType}; #[tokio::main(flavor = "multi_thread")] async fn main() { let (tx, rx) = std::sync::mpsc::channel(); tokio::task::spawn(async move { for msg in rx { println!("{}", msg); } }); // Crawl realtime trades for all symbols of binance inverse_swap markets crawl_trade("binance", MarketType::InverseSwap, None, tx).await; } ``` ## Crawl realtime level2 orderbook incremental updates ```rust use crypto_crawler::{crawl_l2_event, MarketType}; #[tokio::main(flavor = "multi_thread")] async fn main() { let (tx, rx) = std::sync::mpsc::channel(); tokio::task::spawn(async move { for msg in rx { println!("{}", msg); } }); // Crawl realtime level2 incremental updates for all symbols of binance inverse_swap markets crawl_l2_event("binance", MarketType::InverseSwap, None, tx).await; } ``` ## Crawl level2 orderbook full snapshots from RESTful API ```rust use crypto_crawler::{crawl_l2_snapshot, MarketType}; fn main() { let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { for msg in rx { println!("{}", msg); } }); // Crawl level2 full snapshots for all symbols of binance inverse_swap markets crawl_l2_snapshot("binance", MarketType::InverseSwap, None, tx); } ``` ## Crawl realtime level2 orderbook top-K snapshots ```rust use crypto_crawler::{crawl_l2_topk, MarketType}; #[tokio::main(flavor = "multi_thread")] async fn main() { let (tx, rx) = std::sync::mpsc::channel(); tokio::task::spawn(async move { for msg in rx { println!("{}", msg); } }); // Crawl realtime level2 top-k snapshots for all symbols of binance inverse_swap markets crawl_l2_topk("binance", MarketType::InverseSwap, None, tx).await; } ``` ## Crawl realtime level3 orderbook incremental updates ```rust use crypto_crawler::{crawl_l3_event, MarketType}; #[tokio::main(flavor = "multi_thread")] async fn main() { let (tx, rx) = std::sync::mpsc::channel(); tokio::task::spawn(async move { for msg in rx { println!("{}", msg); } }); // Crawl realtime level3 updates for all symbols of CoinbasePro spot market crawl_l3_event("coinbase_pro", MarketType::Spot, None, tx).await; } ``` ## Crawl level3 orderbook full snapshots from RESTful API ```rust use crypto_crawler::{crawl_l3_snapshot, MarketType}; fn main() { let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { for msg in rx { println!("{}", msg); } }); // Crawl level3 orderbook full snapshots for all symbols of CoinbasePro spot markets crawl_l3_snapshot("coinbase_pro", MarketType::Spot, None, tx); } ``` ## Crawl realtime BBO ```rust use crypto_crawler::{crawl_bbo, MarketType}; #[tokio::main(flavor = "multi_thread")] async fn main() { let (tx, rx) = std::sync::mpsc::channel(); tokio::task::spawn(async move { for msg in rx { println!("{}", msg); } }); // Crawl realtime best bid and ask messages for all symbols of binance COIN-margined perpetual markets crawl_bbo("binance", MarketType::InverseSwap, None, tx).await; } ``` ## Crawl 24hr rolling window tickers ```rust use crypto_crawler::{crawl_ticker, MarketType}; #[tokio::main(flavor = "multi_thread")] async fn main() { let (tx, rx) = std::sync::mpsc::channel(); tokio::task::spawn(async move { for msg in rx { println!("{}", msg); } }); // Crawl 24hr rolling window tickers for all symbols of binance COIN-margined perpetual markets crawl_ticker("binance", MarketType::InverseSwap, None, tx).await; } ``` ## Crawl candlesticks(i.e., OHLCV) ```rust use crypto_crawler::{crawl_candlestick, MarketType}; #[tokio::main(flavor = "multi_thread")] async fn main() { let (tx, rx) = std::sync::mpsc::channel(); tokio::task::spawn(async move { for msg in rx { println!("{}", msg); } }); // Crawl candlesticks from 1 minute to 3 minutes for all symbols of binance COIN-margined perpetual markets crawl_candlestick("binance", MarketType::InverseSwap, None, tx).await; } ``` ## Crawl funding rates ```rust use crypto_crawler::{crawl_funding_rate, MarketType}; #[tokio::main(flavor = "multi_thread")] async fn main() { let (tx, rx) = std::sync::mpsc::channel(); tokio::task::spawn(async move { for msg in rx { println!("{}", msg); } }); // Crawl funding rates for all symbols of binance COIN-margined perpetual markets crawl_funding_rate("binance", MarketType::InverseSwap, None, tx).await; } ``` ================================================ FILE: crypto-crawler/src/crawlers/binance.rs ================================================ use core::panic; use std::sync::mpsc::Sender; use crate::{ crawlers::utils::crawl_event, fetch_symbols_retry, get_hot_spot_symbols, msg::Message, utils::cmc_rank::sort_by_cmc_rank, }; use crypto_market_type::MarketType; use crypto_msg_type::MessageType; use crypto_ws_client::*; use super::utils::create_conversion_thread; const EXCHANGE_NAME: &str = "binance"; pub(crate) async fn crawl_trade( market_type: MarketType, symbols: Option<&[String]>, tx: Sender, ) { if market_type == MarketType::EuropeanOption && (symbols.is_none() || symbols.unwrap().is_empty()) { let tx = create_conversion_thread( EXCHANGE_NAME.to_string(), MessageType::Trade, market_type, tx, ); let topics: Vec<(String, String)> = vec![ // ("TICKER_ALL".to_string(), "BTCUSDT".to_string()), ("TRADE_ALL".to_string(), "BTCUSDT_C".to_string()), ("TRADE_ALL".to_string(), "BTCUSDT_P".to_string()), ]; let ws_client = BinanceOptionWSClient::new(tx, None).await; ws_client.subscribe(&topics).await; ws_client.run().await; ws_client.close().await; } else { crawl_event(EXCHANGE_NAME, MessageType::Trade, market_type, symbols, tx).await; } } pub(crate) async fn crawl_bbo( market_type: MarketType, symbols: Option<&[String]>, tx: Sender, ) { if symbols.is_none() || symbols.unwrap().is_empty() { if market_type == MarketType::Spot { // spot `!bookTicker` has been removed since December 7, 2022 let mut hot_spot_symbols = tokio::task::block_in_place(move || { let spot_symbols = fetch_symbols_retry(EXCHANGE_NAME, market_type); get_hot_spot_symbols(EXCHANGE_NAME, &spot_symbols) }); sort_by_cmc_rank(EXCHANGE_NAME, &mut hot_spot_symbols); let symbols = Some(hot_spot_symbols.as_slice()); crawl_event(EXCHANGE_NAME, MessageType::BBO, market_type, symbols, tx).await } else { let tx = create_conversion_thread( EXCHANGE_NAME.to_string(), MessageType::BBO, market_type, tx, ); let commands = vec![r#"{"id":9527,"method":"SUBSCRIBE","params":["!bookTicker"]}"#.to_string()]; // All Book Tickers Stream match market_type { MarketType::InverseFuture | MarketType::InverseSwap => { let ws_client = BinanceInverseWSClient::new(tx, None).await; ws_client.send(&commands).await; ws_client.run().await; ws_client.close().await; } MarketType::LinearFuture | MarketType::LinearSwap => { let ws_client = BinanceLinearWSClient::new(tx, None).await; ws_client.send(&commands).await; ws_client.run().await; ws_client.close().await; } _ => panic!("Binance {} market does NOT have the BBO channel", market_type), } } } else { crawl_event(EXCHANGE_NAME, MessageType::BBO, market_type, symbols, tx).await; } } pub(crate) async fn crawl_ticker( market_type: MarketType, symbols: Option<&[String]>, tx: Sender, ) { if symbols.is_none() || symbols.unwrap().is_empty() { let tx = create_conversion_thread( EXCHANGE_NAME.to_string(), MessageType::Ticker, market_type, tx, ); let commands = vec![r#"{"id":9527,"method":"SUBSCRIBE","params":["!ticker@arr"]}"#.to_string()]; match market_type { MarketType::Spot => { let ws_client = BinanceSpotWSClient::new(tx, None).await; ws_client.send(&commands).await; ws_client.run().await; ws_client.close().await; } MarketType::InverseFuture | MarketType::InverseSwap => { let ws_client = BinanceInverseWSClient::new(tx, None).await; ws_client.send(&commands).await; ws_client.run().await; ws_client.close().await; } MarketType::LinearFuture | MarketType::LinearSwap => { let ws_client = BinanceLinearWSClient::new(tx, None).await; ws_client.send(&commands).await; ws_client.run().await; ws_client.close().await; } MarketType::EuropeanOption => { let commands = vec![ r#"{"id":9527,"method":"SUBSCRIBE","params":["BTCUSDT@TICKER_ALL"]}"# .to_string(), ]; let ws_client = BinanceLinearWSClient::new(tx, None).await; ws_client.send(&commands).await; ws_client.run().await; ws_client.close().await; } _ => panic!("Binance {} market does NOT have the ticker channel", market_type), } } else { crawl_event(EXCHANGE_NAME, MessageType::Ticker, market_type, symbols, tx).await; } } #[allow(clippy::unnecessary_unwrap)] pub(crate) async fn crawl_funding_rate( market_type: MarketType, symbols: Option<&[String]>, tx: Sender, ) { let tx = create_conversion_thread( EXCHANGE_NAME.to_string(), MessageType::FundingRate, market_type, tx, ); let ws_client: Box = match market_type { MarketType::InverseSwap => Box::new(BinanceInverseWSClient::new(tx, None).await), MarketType::LinearSwap => Box::new(BinanceLinearWSClient::new(tx, None).await), _ => panic!("Binance {} does NOT have funding rates", market_type), }; if symbols.is_none() || symbols.unwrap().is_empty() { let commands = vec![r#"{"id":9527,"method":"SUBSCRIBE","params":["!markPrice@arr"]}"#.to_string()]; ws_client.send(&commands).await; } else { let topics = symbols .unwrap() .iter() .map(|symbol| ("markPrice".to_string(), symbol.to_string())) .collect::>(); ws_client.subscribe(&topics).await; }; ws_client.run().await; ws_client.close().await; } ================================================ FILE: crypto-crawler/src/crawlers/bitmex.rs ================================================ use super::{ crawl_candlestick_ext, crawl_event, utils::{check_args, fetch_symbols_retry}, }; use crate::{crawlers::utils::create_conversion_thread, msg::Message}; use crypto_market_type::MarketType; use crypto_msg_type::MessageType; use crypto_ws_client::*; use std::sync::mpsc::Sender; const EXCHANGE_NAME: &str = "bitmex"; async fn crawl_all(msg_type: MessageType, tx: Sender) { let tx = create_conversion_thread(EXCHANGE_NAME.to_string(), msg_type, MarketType::Unknown, tx); let channel: &str = match msg_type { MessageType::Trade => "trade", MessageType::L2Event => "orderBookL2_25", MessageType::L2TopK => "orderBook10", MessageType::BBO => "quote", MessageType::L2Snapshot => "orderBookL2", MessageType::FundingRate => "funding", _ => panic!("unsupported message type {msg_type}"), }; let commands = vec![format!(r#"{{"op":"subscribe","args":["{channel}"]}}"#)]; let ws_client = BitmexWSClient::new(tx, None).await; ws_client.send(&commands).await; ws_client.run().await; ws_client.close().await; } pub(crate) async fn crawl_trade( market_type: MarketType, symbols: Option<&[String]>, tx: Sender, ) { if market_type == MarketType::Unknown { // crawl all symbols crawl_all(MessageType::Trade, tx).await; } else { crawl_event(EXCHANGE_NAME, MessageType::Trade, market_type, symbols, tx).await; } } pub(crate) async fn crawl_l2_event( market_type: MarketType, symbols: Option<&[String]>, tx: Sender, ) { if market_type == MarketType::Unknown { // crawl all symbols crawl_all(MessageType::L2Event, tx).await; } else { crawl_event(EXCHANGE_NAME, MessageType::L2Event, market_type, symbols, tx).await; } } pub(crate) async fn crawl_bbo( market_type: MarketType, symbols: Option<&[String]>, tx: Sender, ) { if market_type == MarketType::Unknown { // crawl all symbols crawl_all(MessageType::BBO, tx).await; } else { crawl_event(EXCHANGE_NAME, MessageType::BBO, market_type, symbols, tx).await; } } pub(crate) async fn crawl_l2_topk( market_type: MarketType, symbols: Option<&[String]>, tx: Sender, ) { if market_type == MarketType::Unknown { // crawl all symbols crawl_all(MessageType::L2TopK, tx).await; } else { crawl_event(EXCHANGE_NAME, MessageType::L2TopK, market_type, symbols, tx).await; } } #[allow(clippy::unnecessary_unwrap)] pub(crate) async fn crawl_funding_rate( market_type: MarketType, symbols: Option<&[String]>, tx: Sender, ) { if market_type == MarketType::Unknown { // crawl all symbols crawl_all(MessageType::FundingRate, tx).await; } else { let is_empty = match symbols { Some(list) => { if list.is_empty() { true } else { check_args(EXCHANGE_NAME, market_type, list); false } } None => true, }; let real_symbols = if is_empty { tokio::task::block_in_place(move || fetch_symbols_retry(EXCHANGE_NAME, market_type)) } else { symbols.unwrap().to_vec() }; if real_symbols.is_empty() { panic!("real_symbols is empty"); } let tx = create_conversion_thread( EXCHANGE_NAME.to_string(), MessageType::FundingRate, market_type, tx, ); let topics: Vec<(String, String)> = real_symbols.iter().map(|symbol| ("funding".to_string(), symbol.to_string())).collect(); match market_type { MarketType::InverseSwap | MarketType::QuantoSwap => { let ws_client = BitmexWSClient::new(tx, None).await; ws_client.subscribe(&topics).await; ws_client.run().await; ws_client.close().await; } _ => panic!("BitMEX {market_type} does NOT have funding rates"), } } } pub(crate) async fn crawl_candlestick( market_type: MarketType, symbol_interval_list: Option<&[(String, usize)]>, tx: Sender, ) { if market_type == MarketType::Unknown { let tx = create_conversion_thread( EXCHANGE_NAME.to_string(), MessageType::Candlestick, market_type, tx, ); let commands = vec![ r#"{"op":"subscribe","args":["tradeBin1m"]}"#.to_string(), r#"{"op":"subscribe","args":["tradeBin5m"]}"#.to_string(), ]; let ws_client = BitmexWSClient::new(tx, None).await; ws_client.send(&commands).await; ws_client.run().await; ws_client.close().await; } else { crawl_candlestick_ext(EXCHANGE_NAME, market_type, symbol_interval_list, tx).await; } } ================================================ FILE: crypto-crawler/src/crawlers/deribit.rs ================================================ use super::crawl_event; use crate::{crawlers::utils::create_conversion_thread, msg::Message}; use crypto_market_type::MarketType; use crypto_msg_type::MessageType; use crypto_ws_client::*; use std::sync::mpsc::Sender; const EXCHANGE_NAME: &str = "deribit"; pub(crate) async fn crawl_trade( market_type: MarketType, symbols: Option<&[String]>, tx: Sender, ) { if symbols.is_none() || symbols.unwrap().is_empty() { let tx = create_conversion_thread( EXCHANGE_NAME.to_string(), MessageType::Trade, market_type, tx, ); // "any" menas all, see https://docs.deribit.com/?javascript#trades-kind-currency-interval let topics: Vec<(String, String)> = match market_type { MarketType::InverseFuture => { vec![("trades.future.SYMBOL.100ms".to_string(), "any".to_string())] } MarketType::InverseSwap => { vec![ ("trades.SYMBOL.100ms".to_string(), "BTC-PERPETUAL".to_string()), ("trades.SYMBOL.100ms".to_string(), "ETH-PERPETUAL".to_string()), ] } MarketType::EuropeanOption => { vec![("trades.option.SYMBOL.100ms".to_string(), "any".to_string())] } _ => panic!("Deribit does NOT have the {market_type} market type"), }; let ws_client = DeribitWSClient::new(tx, None).await; ws_client.subscribe(&topics).await; ws_client.run().await; ws_client.close().await; } else { crawl_event(EXCHANGE_NAME, MessageType::Trade, market_type, symbols, tx).await; } } ================================================ FILE: crypto-crawler/src/crawlers/huobi.rs ================================================ use super::utils::fetch_symbols_retry; use crate::{ crawlers::{crawl_event, utils::create_conversion_thread}, msg::Message, }; use crypto_market_type::MarketType; use crypto_msg_type::MessageType; use crypto_ws_client::*; use std::sync::mpsc::Sender; const EXCHANGE_NAME: &str = "huobi"; #[allow(clippy::unnecessary_unwrap)] pub(crate) async fn crawl_l2_event( market_type: MarketType, symbols: Option<&[String]>, tx: Sender, ) { match market_type { MarketType::Spot => { let tx = create_conversion_thread( EXCHANGE_NAME.to_string(), MessageType::L2Event, market_type, tx, ); let symbols: Vec = if symbols.is_none() || symbols.unwrap().is_empty() { tokio::task::block_in_place(move || fetch_symbols_retry(EXCHANGE_NAME, market_type)) } else { symbols.unwrap().to_vec() }; // Huobi Spot market.$symbol.mbp.$levels must use wss://api.huobi.pro/feed // or wss://api-aws.huobi.pro/feed let ws_client = HuobiSpotWSClient::new(tx, Some("wss://api.huobi.pro/feed")).await; ws_client.subscribe_orderbook(&symbols).await; ws_client.run().await; ws_client.close().await; } MarketType::InverseFuture | MarketType::LinearSwap | MarketType::InverseSwap | MarketType::EuropeanOption => { crawl_event(EXCHANGE_NAME, MessageType::L2Event, market_type, symbols, tx).await } _ => panic!("Huobi does NOT have the {market_type} market type"), } } #[allow(clippy::unnecessary_unwrap)] pub(crate) async fn crawl_funding_rate( market_type: MarketType, symbols: Option<&[String]>, tx: Sender, ) { let tx = create_conversion_thread( EXCHANGE_NAME.to_string(), MessageType::FundingRate, market_type, tx, ); let symbols: Vec = if symbols.is_none() || symbols.unwrap().is_empty() { vec!["*".to_string()] } else { symbols.unwrap().to_vec() }; let commands: Vec = symbols .into_iter() .map(|symbol| format!(r#"{{"topic":"public.{symbol}.funding_rate","op":"sub"}}"#)) .collect(); match market_type { MarketType::InverseSwap => { let ws_client = HuobiInverseSwapWSClient::new(tx, Some("wss://api.hbdm.com/swap-notification")) .await; ws_client.send(&commands).await; ws_client.run().await; ws_client.close().await; } MarketType::LinearSwap => { let ws_client = HuobiLinearSwapWSClient::new( tx, Some("wss://api.hbdm.com/linear-swap-notification"), ) .await; ws_client.send(&commands).await; ws_client.run().await; ws_client.close().await; } _ => panic!("Huobi {market_type} does NOT have funding rates"), } } ================================================ FILE: crypto-crawler/src/crawlers/kucoin.rs ================================================ use crate::{crawlers::utils::create_conversion_thread, msg::Message}; use crypto_market_type::MarketType; use crypto_msg_type::MessageType; use crypto_ws_client::*; use std::sync::mpsc::Sender; use super::crawl_event; const EXCHANGE_NAME: &str = "kucoin"; pub(crate) async fn crawl_bbo( market_type: MarketType, symbols: Option<&[String]>, tx: Sender, ) { if market_type == MarketType::Spot && (symbols.is_none() || symbols.unwrap().is_empty()) { let tx = create_conversion_thread(EXCHANGE_NAME.to_string(), MessageType::BBO, market_type, tx); // https://docs.kucoin.com/#all-symbols-ticker let commands: Vec = vec![r#"{"id":"crypto-ws-client","type":"subscribe","topic":"/market/ticker:all","privateChannel":false,"response":true}"#.to_string()]; let ws_client = KuCoinSpotWSClient::new(tx, None).await; ws_client.send(&commands).await; ws_client.run().await; ws_client.close().await; } else { crawl_event(EXCHANGE_NAME, MessageType::BBO, market_type, symbols, tx).await; } } ================================================ FILE: crypto-crawler/src/crawlers/mod.rs ================================================ #[macro_use] mod utils; pub(super) mod binance; pub(super) mod bitmex; pub(super) mod deribit; pub(super) mod huobi; pub(super) mod kucoin; pub(super) mod okx; pub(super) mod zb; pub(super) mod zbg; pub use utils::fetch_symbols_retry; pub(super) use utils::{ crawl_candlestick_ext, crawl_event, crawl_open_interest, crawl_snapshot, create_ws_client_symbol, }; ================================================ FILE: crypto-crawler/src/crawlers/okx.rs ================================================ use super::utils::fetch_symbols_retry; use crate::{crawlers::utils::create_conversion_thread, msg::Message}; use crypto_market_type::MarketType; use crypto_msg_type::MessageType; use crypto_ws_client::*; use std::sync::mpsc::Sender; const EXCHANGE_NAME: &str = "okx"; #[allow(clippy::unnecessary_unwrap)] pub(crate) async fn crawl_funding_rate( market_type: MarketType, symbols: Option<&[String]>, tx: Sender, ) { let tx = create_conversion_thread( EXCHANGE_NAME.to_string(), MessageType::FundingRate, market_type, tx, ); let symbols: Vec = if symbols.is_none() || symbols.unwrap().is_empty() { tokio::task::block_in_place(move || fetch_symbols_retry(EXCHANGE_NAME, market_type)) } else { symbols.unwrap().to_vec() }; let topics: Vec<(String, String)> = symbols.into_iter().map(|symbol| ("funding-rate".to_string(), symbol)).collect(); match market_type { MarketType::InverseSwap | MarketType::LinearSwap => { let ws_client = OkxWSClient::new(tx, None).await; ws_client.subscribe(&topics).await; ws_client.run().await; ws_client.close().await; } _ => panic!("OKX {market_type} does NOT have funding rates"), } } #[deprecated(since = "4.1.2", note = "OKX open interest is fetched via HTTP for now")] #[allow(dead_code)] pub(crate) async fn crawl_open_interest( market_type: MarketType, symbols: Option<&[String]>, tx: Sender, ) { let tx = create_conversion_thread( EXCHANGE_NAME.to_string(), MessageType::OpenInterest, market_type, tx, ); let symbols = if let Some(symbols) = symbols { if symbols.is_empty() { tokio::task::block_in_place(move || fetch_symbols_retry(EXCHANGE_NAME, market_type)) } else { symbols.to_vec() } } else { tokio::task::block_in_place(move || fetch_symbols_retry(EXCHANGE_NAME, market_type)) }; let topics: Vec<(String, String)> = symbols.into_iter().map(|symbol| ("open-interest".to_string(), symbol)).collect(); if market_type != MarketType::Spot { let ws_client = OkxWSClient::new(tx, None).await; ws_client.subscribe(&topics).await; ws_client.run().await; ws_client.close().await; } else { panic!("spot does NOT have open interest"); } } ================================================ FILE: crypto-crawler/src/crawlers/utils.rs ================================================ use std::{ sync::{mpsc::Sender, Arc}, time::{Duration, SystemTime, UNIX_EPOCH}, }; use crate::utils::{REST_LOCKS, WS_LOCKS}; use crypto_market_type::{get_market_types, MarketType}; use crypto_markets::fetch_symbols; use crypto_rest_client::{fetch_l2_snapshot, fetch_l3_snapshot, fetch_open_interest}; use crypto_ws_client::*; use log::*; use crate::{get_hot_spot_symbols, utils::cmc_rank::sort_by_cmc_rank, Message, MessageType}; pub fn fetch_symbols_retry(exchange: &str, market_type: MarketType) -> Vec { let retry_count = std::env::var("REST_RETRY_COUNT") .unwrap_or_else(|_| "5".to_string()) .parse::() .unwrap(); let cooldown_time = get_cooldown_time_per_request(exchange, market_type); let lock = REST_LOCKS.get(exchange).unwrap().get(&market_type).unwrap().clone(); let mut symbols = Vec::::new(); let mut backoff_factor = 1; for i in 0..retry_count { let mut lock_ = lock.lock().unwrap(); if !lock_.owns_lock() { lock_.lock().unwrap(); } match fetch_symbols(exchange, market_type) { Ok(list) => { symbols = list; if lock_.owns_lock() { lock_.unlock().unwrap(); } break; } Err(err) => { backoff_factor *= 2; if i == retry_count - 1 { error!("The {}th time, {}", i, err); } else { warn!("The {}th time, {}", i, err); } } } // Cooldown after each request, and make all other processes wait // on the lock to avoid parallel requests, thus avoid 429 error std::thread::sleep(cooldown_time * backoff_factor); if lock_.owns_lock() { lock_.unlock().unwrap(); } } symbols } pub(super) fn check_args(exchange: &str, market_type: MarketType, symbols: &[String]) { let market_types = get_market_types(exchange); if !market_types.contains(&market_type) { panic!("{exchange} does NOT have the {market_type} market type"); } let valid_symbols = fetch_symbols_retry(exchange, market_type); let invalid_symbols: Vec = symbols.iter().filter(|symbol| !valid_symbols.contains(symbol)).cloned().collect(); if !invalid_symbols.is_empty() { panic!( "Invalid symbols: {}, {} {} available trading symbols are {}", invalid_symbols.join(","), exchange, market_type, valid_symbols.join(",") ); } } fn get_cooldown_time_per_request(exchange: &str, market_type: MarketType) -> Duration { let millis = match exchange { "binance" => 500, // spot weitht 1200, contract weight 2400 "bitget" => 100, // 20 requests per 2 seconds "bithumb" => 8 * 10, /* 135 requests per 1 second for public APIs, multiplied by 10 to */ // reduce its frequency "bitmex" => 2000, /* 60 requests per minute on all routes (reduced to 30 when */ // unauthenticated) "bitstamp" => 75 * 10, /* 8000 requests per 10 minutes, but bitstamp orderbook is too */ // big, need to reduce its frequency "bitz" => 34, // no more than 30 times within 1 second "bybit" => 20 * 10, /* 50 requests per second continuously for 2 minutes, multiplied */ // by 10 to reduce its frequency "coinbase_pro" => 100, // 10 requests per second "deribit" => 50, // 20 requests per second "dydx" => 100, // 100 requests per 10 seconds "gate" => 4, // 300 read operations per IP per second "huobi" => 2, // 800 times/second for one IP "kucoin" => match market_type { MarketType::Spot => 300, // 3x to avoid 429 _ => 100, // 30 times/3s }, "mexc" => 100, // 20 times per 2 seconds "okx" => 100, // 20 requests per 2 seconds _ => 100, }; Duration::from_millis(millis) } /// Crawl leve2 or level3 orderbook snapshots through RESTful APIs. pub(crate) fn crawl_snapshot( exchange: &str, market_type: MarketType, msg_type: MessageType, // L2Snapshot or L3Snapshot symbols: Option<&[String]>, tx: Sender, ) { let is_empty = match symbols { Some(list) => { if list.is_empty() { true } else { check_args(exchange, market_type, list); false } } None => true, }; let cooldown_time = get_cooldown_time_per_request(exchange, market_type); let lock = REST_LOCKS.get(exchange).unwrap().get(&market_type).unwrap().clone(); 'outer: loop { let mut real_symbols = if is_empty { if market_type == MarketType::Spot { let spot_symbols = fetch_symbols_retry(exchange, market_type); get_hot_spot_symbols(exchange, &spot_symbols) } else { fetch_symbols_retry(exchange, market_type) } } else { symbols.unwrap().to_vec() }; sort_by_cmc_rank(exchange, &mut real_symbols); let mut index = 0_usize; let mut success_count = 0_u64; let mut backoff_factor = 1; // retry 5 times at most while index < real_symbols.len() && backoff_factor < 6 { let symbol = real_symbols[index].as_str(); let mut lock_ = lock.lock().unwrap(); if !lock_.owns_lock() { lock_.lock().unwrap(); } let resp = match msg_type { MessageType::L2Snapshot => fetch_l2_snapshot(exchange, market_type, symbol, None), MessageType::L3Snapshot => fetch_l3_snapshot(exchange, market_type, symbol, None), _ => panic!("msg_type must be L2Snapshot or L3Snapshot"), }; // Cooldown after each request, and make all other processes wait // on the lock to avoid parallel requests, thus avoid 429 error std::thread::sleep(cooldown_time); if lock_.owns_lock() { lock_.unlock().unwrap(); } match resp { Ok(msg) => { index += 1; success_count += 1; backoff_factor = 1; let message = Message::new_with_symbol( exchange.to_string(), market_type, msg_type, symbol.to_string(), msg, ); if tx.send(message).is_err() { // break the loop if there is no receiver break 'outer; } } Err(err) => { let current_timestamp = SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .unwrap() .as_millis() as u64; warn!( "{} {} {} {} {} {}, error: {}, back off for {} milliseconds", current_timestamp, success_count, backoff_factor, exchange, market_type, symbol, err, (backoff_factor * cooldown_time).as_millis() ); std::thread::sleep(backoff_factor * cooldown_time); success_count = 0; backoff_factor += 1; } } } std::thread::sleep(cooldown_time * 2); // if real_symbols is empty, CPU will be 100% without this line } } /// Crawl open interests of all trading symbols. pub(crate) fn crawl_open_interest(exchange: &str, market_type: MarketType, tx: Sender) { let cooldown_time = get_cooldown_time_per_request(exchange, market_type); let lock = REST_LOCKS.get(exchange).unwrap().get(&market_type).unwrap().clone(); 'outer: loop { match exchange { "bitz" | "deribit" | "dydx" | "ftx" | "huobi" | "kucoin" | "okx" => { let mut lock_ = lock.lock().unwrap(); if !lock_.owns_lock() { lock_.lock().unwrap(); } let resp = fetch_open_interest(exchange, market_type, None); if let Ok(json) = resp { if exchange == "deribit" { // A RESTful response of deribit open_interest contains four lines for x in json.trim().split('\n') { let message = Message::new( exchange.to_string(), market_type, MessageType::OpenInterest, x.to_string(), ); if tx.send(message).is_err() { break; // break the loop if there is no receiver } } } else { let message = Message::new( exchange.to_string(), market_type, MessageType::OpenInterest, json, ); if tx.send(message).is_err() { break; // break the loop if there is no receiver } } } // Cooldown after each request, and make all other processes wait // on the lock to avoid parallel requests, thus avoid 429 error std::thread::sleep(cooldown_time); if lock_.owns_lock() { lock_.unlock().unwrap(); } } "binance" | "bitget" | "bybit" | "gate" | "zbg" => { let real_symbols = fetch_symbols_retry(exchange, market_type); let mut index = 0_usize; let mut success_count = 0_u64; let mut backoff_factor = 1; // retry 5 times at most while index < real_symbols.len() && backoff_factor < 6 { let symbol = real_symbols[index].as_str(); let mut lock_ = lock.lock().unwrap(); if !lock_.owns_lock() { lock_.lock().unwrap(); } let resp = fetch_open_interest(exchange, market_type, Some(symbol)); // Cooldown after each request, and make all other processes wait // on the lock to avoid parallel requests, thus avoid 429 error std::thread::sleep(cooldown_time); if lock_.owns_lock() { lock_.unlock().unwrap(); } match resp { Ok(msg) => { index += 1; success_count += 1; backoff_factor = 1; let message = Message::new_with_symbol( exchange.to_string(), market_type, MessageType::OpenInterest, symbol.to_string(), msg, ); if tx.send(message).is_err() { // break the loop if there is no receiver break 'outer; } } Err(err) => { let current_timestamp = SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .unwrap() .as_millis() as u64; warn!( "{} {} {} {} {} {}, error: {}, back off for {} milliseconds", current_timestamp, success_count, backoff_factor, exchange, market_type, symbol, err, (backoff_factor * cooldown_time).as_millis() ); std::thread::sleep(backoff_factor * cooldown_time); success_count = 0; backoff_factor += 1; } } } } _ => panic!("{exchange} does NOT have open interest RESTful API"), } std::thread::sleep(cooldown_time * 2); // if real_symbols is empty, CPU will be 100% without this line } } async fn subscribe_with_lock( exchange: String, market_type: MarketType, msg_type: MessageType, symbols: Vec, ws_client: Arc, ) { match msg_type { MessageType::BBO => ws_client.subscribe_bbo(&symbols).await, MessageType::Trade => ws_client.subscribe_trade(&symbols).await, MessageType::L2Event => ws_client.subscribe_orderbook(&symbols).await, MessageType::L3Event => ws_client.subscribe_l3_orderbook(&symbols).await, MessageType::L2TopK => ws_client.subscribe_orderbook_topk(&symbols).await, MessageType::Ticker => ws_client.subscribe_ticker(&symbols).await, _ => panic!("{exchange} {market_type} does NOT have {msg_type} websocket channel"), }; } fn get_connection_interval_ms(exchange: &str, _market_type: MarketType) -> Option { match exchange { "bitfinex" => Some(3000), /* you cannot open more than 20 connections per minute, see https://docs.bitfinex.com/docs/requirements-and-limitations#websocket-rate-limits */ // "bitmex" => Some(9000), // 40 per hour "bitz" => Some(100), /* `cat crawler-trade-bitz-spot-error-12.log` has many "429 Too */ // Many Requests" "kucoin" => Some(2000), /* Connection Limit: 30 per minute, see https://docs.kucoin.com/#connection-times */ "okx" => Some(1000), /* Connection limit: 1 time per second, https://www.okx.com/docs-v5/en/#websocket-api-connect */ _ => None, } } fn get_num_subscriptions_per_connection(exchange: &str, market_type: MarketType) -> usize { match exchange { // A single connection can listen to a maximum of 200 streams "binance" => { if market_type == MarketType::Spot { // https://binance-docs.github.io/apidocs/spot/en/#websocket-limits 1024 } else { // https://binance-docs.github.io/apidocs/futures/en/#websocket-market-streams // https://binance-docs.github.io/apidocs/delivery/en/#websocket-market-streams 200 } } // https://binance-docs.github.io/apidocs/futures/en/#websocket-market-streams // All websocket connections have a limit of 30 subscriptions to public market data feed // channels "bitfinex" => 30, // https://docs.bitfinex.com/docs/ws-general#subscribe-to-channels "kucoin" => 300, /* Subscription limit for each connection: 300 topics, see https://docs.kucoin.com/#topic-subscription-limit */ "okx" => 256, // okx spot l2_event throws many ResetWithoutClosingHandshake errors _ => usize::MAX, // usize::MAX means unlimited } } async fn create_ws_client_internal( exchange: &str, market_type: MarketType, tx: Sender, ) -> Arc { match exchange { "binance" => match market_type { MarketType::Spot => Arc::new(BinanceSpotWSClient::new(tx, None).await), MarketType::InverseFuture | MarketType::InverseSwap => { Arc::new(BinanceInverseWSClient::new(tx, None).await) } MarketType::LinearFuture | MarketType::LinearSwap => { Arc::new(BinanceLinearWSClient::new(tx, None).await) } MarketType::EuropeanOption => Arc::new(BinanceOptionWSClient::new(tx, None).await), _ => panic!("Binance does NOT have the {market_type} market type"), }, "bitfinex" => Arc::new(BitfinexWSClient::new(tx, None).await), "bitget" => match market_type { MarketType::Spot => Arc::new(BitgetSpotWSClient::new(tx, None).await), MarketType::InverseFuture | MarketType::InverseSwap | MarketType::LinearSwap => { Arc::new(BitgetSwapWSClient::new(tx, None).await) } _ => panic!("Bitget does NOT have the {market_type} market type"), }, "bithumb" => Arc::new(BithumbWSClient::new(tx, None).await), "bitmex" => Arc::new(BitmexWSClient::new(tx, None).await), "bitstamp" => Arc::new(BitstampWSClient::new(tx, None).await), "bitz" => match market_type { MarketType::Spot => Arc::new(BitzSpotWSClient::new(tx, None).await), _ => panic!("Bitz does NOT have the {market_type} market type"), }, "bybit" => match market_type { MarketType::InverseFuture | MarketType::InverseSwap => { Arc::new(BybitInverseWSClient::new(tx, None).await) } MarketType::LinearSwap => Arc::new(BybitLinearSwapWSClient::new(tx, None).await), _ => panic!("Bybit does NOT have the {market_type} market type"), }, "coinbase_pro" => Arc::new(CoinbaseProWSClient::new(tx, None).await), "deribit" => Arc::new(DeribitWSClient::new(tx, None).await), "dydx" => match market_type { MarketType::LinearSwap => Arc::new(DydxSwapWSClient::new(tx, None).await), _ => panic!("dYdX does NOT have the {market_type} market type"), }, "ftx" => Arc::new(FtxWSClient::new(tx, None).await), "gate" => match market_type { MarketType::Spot => Arc::new(GateSpotWSClient::new(tx, None).await), MarketType::InverseSwap => Arc::new(GateInverseSwapWSClient::new(tx, None).await), MarketType::LinearSwap => Arc::new(GateLinearSwapWSClient::new(tx, None).await), MarketType::InverseFuture => Arc::new(GateInverseFutureWSClient::new(tx, None).await), MarketType::LinearFuture => Arc::new(GateLinearFutureWSClient::new(tx, None).await), _ => panic!("Gate does NOT have the {market_type} market type"), }, "huobi" => match market_type { MarketType::Spot => Arc::new(HuobiSpotWSClient::new(tx, None).await), MarketType::InverseFuture => Arc::new(HuobiFutureWSClient::new(tx, None).await), MarketType::LinearSwap => Arc::new(HuobiLinearSwapWSClient::new(tx, None).await), MarketType::InverseSwap => Arc::new(HuobiInverseSwapWSClient::new(tx, None).await), MarketType::EuropeanOption => Arc::new(HuobiOptionWSClient::new(tx, None).await), _ => panic!("Huobi does NOT have the {market_type} market type"), }, "kraken" => match market_type { MarketType::Spot => Arc::new(KrakenSpotWSClient::new(tx, None).await), MarketType::InverseFuture | MarketType::InverseSwap => { Arc::new(KrakenFuturesWSClient::new(tx, None).await) } _ => panic!("Kraken does NOT have the {market_type} market type"), }, "kucoin" => match market_type { MarketType::Spot => Arc::new(KuCoinSpotWSClient::new(tx, None).await), MarketType::InverseSwap | MarketType::LinearSwap | MarketType::InverseFuture => { Arc::new(KuCoinSwapWSClient::new(tx, None).await) } _ => panic!("KuCoin does NOT have the {market_type} market type"), }, "mexc" => match market_type { MarketType::Spot => Arc::new(MexcSpotWSClient::new(tx, None).await), MarketType::LinearSwap | MarketType::InverseSwap => { Arc::new(MexcSwapWSClient::new(tx, None).await) } _ => panic!("MEXC does NOT have the {market_type} market type"), }, "okx" => Arc::new(OkxWSClient::new(tx, None).await), "zb" => match market_type { MarketType::Spot => Arc::new(ZbSpotWSClient::new(tx, None).await), MarketType::LinearSwap => Arc::new(ZbSwapWSClient::new(tx, None).await), _ => panic!("ZB does NOT have the {market_type} market type"), }, "zbg" => match market_type { MarketType::Spot => Arc::new(ZbgSpotWSClient::new(tx, None).await), MarketType::InverseSwap | MarketType::LinearSwap => { Arc::new(ZbgSwapWSClient::new(tx, None).await) } _ => panic!("ZBG does NOT have the {market_type} market type"), }, _ => panic!("Unknown exchange {exchange}"), } } async fn create_ws_client( exchange: &str, market_type: MarketType, msg_type: MessageType, tx: Sender, ) -> Arc { let tx = create_conversion_thread(exchange.to_string(), msg_type, market_type, tx); if let Some(interval) = get_connection_interval_ms(exchange, market_type) { let lock = WS_LOCKS.get(exchange).unwrap().get(&market_type).unwrap().clone(); let mut lock = lock.lock().await; let mut i = 0; while !lock.owns_lock() { i += 1; debug!( "{} {} {} try_lock_with_pid() the {}th time", exchange, market_type, msg_type, i ); if lock.try_lock_with_pid().is_ok() { break; } else { tokio::time::sleep(std::time::Duration::from_millis( rand::random::() % 90 + 11, )) .await; // give chances to other tasks } } let ws_client = create_ws_client_internal(exchange, market_type, tx).await; tokio::time::sleep(Duration::from_millis(interval)).await; if lock.owns_lock() { lock.unlock().unwrap(); } ws_client } else { create_ws_client_internal(exchange, market_type, tx).await } } pub(crate) async fn create_ws_client_symbol( exchange: &str, market_type: MarketType, tx: Sender, ) -> Arc { let tx = create_parser_thread(exchange.to_string(), market_type, tx); create_ws_client_internal(exchange, market_type, tx).await } #[derive(Clone)] struct EmptyStruct {} // for stop channel fn create_symbol_discovery_thread( exchange: String, market_type: MarketType, subscribed_symbols: Vec, mut stop_ch_rx: tokio::sync::broadcast::Receiver, tx: tokio::sync::mpsc::Sender>, // send out new symbols ) -> tokio::task::JoinHandle<()> { let num_topics_per_connection = get_num_subscriptions_per_connection(&exchange, market_type); let mut subscribed_symbols = subscribed_symbols; let mut num_subscribed_of_last_client = subscribed_symbols.len() % num_topics_per_connection; let mut hourly = tokio::time::interval(Duration::from_secs(3600)); tokio::task::spawn(async move { loop { tokio::select! { _ = stop_ch_rx.recv() => { break; } _ = hourly.tick() => { let exchange_clone = exchange.to_string(); let latest_symbols = tokio::task::block_in_place(move || { fetch_symbols_retry(&exchange_clone, market_type) }); let mut new_symbols: Vec = latest_symbols .iter() .filter(|s| !subscribed_symbols.contains(s)) .cloned() .collect(); if !new_symbols.is_empty() { warn!("Found new symbols: {}", new_symbols.join(", ")); if tx.send(new_symbols.clone()).await.is_err() { break; // break the loop if there is no receiver } num_subscribed_of_last_client += new_symbols.len(); subscribed_symbols.append(&mut new_symbols); } if num_subscribed_of_last_client >= num_topics_per_connection { panic!( "The last connection has subscribed {num_subscribed_of_last_client} topics, which is more than {num_topics_per_connection}, restarting the process", ); // pm2 will restart the whole process } } } } }) } fn create_new_symbol_receiver_thread( exchange: String, msg_type: MessageType, market_type: MarketType, mut symbols_rx: tokio::sync::mpsc::Receiver>, ws_client: Arc, ) -> tokio::task::JoinHandle<()> { tokio::task::spawn(async move { let exchange_clone = exchange; while let Some(new_symbols) = symbols_rx.recv().await { subscribe_with_lock( exchange_clone.clone(), market_type, msg_type, new_symbols, ws_client.clone(), ) .await; } }) } fn create_new_symbol_receiver_thread_candlestick( intervals: Vec, mut rx: tokio::sync::mpsc::Receiver>, ws_client: Arc, ) -> tokio::task::JoinHandle<()> { tokio::task::spawn(async move { while let Some(new_symbols) = rx.recv().await { let new_symbol_interval_list = new_symbols .iter() .flat_map(|symbol| { intervals.clone().into_iter().map(move |interval| (symbol.clone(), interval)) }) .collect::>(); ws_client.subscribe_candlestick(&new_symbol_interval_list).await; } }) } // create a thread to convert Sender Sender pub(crate) fn create_conversion_thread( exchange: String, msg_type: MessageType, market_type: MarketType, tx: Sender, ) -> Sender { let (tx_raw, rx_raw) = std::sync::mpsc::channel(); tokio::task::spawn_blocking(move || { for json in rx_raw { let msg = Message::new(exchange.clone(), market_type, msg_type, json); if tx.send(msg).is_err() { break; // break the loop if there is no receiver } } }); tx_raw } // create a thread to call `crypto-msg-parser` fn create_parser_thread( exchange: String, market_type: MarketType, tx: Sender, ) -> Sender { let (tx_raw, rx_raw) = std::sync::mpsc::channel::(); std::thread::spawn(move || { for json in rx_raw { let msg_type = crypto_msg_parser::get_msg_type(&exchange, &json); let parsed = match msg_type { MessageType::Trade => serde_json::to_string( &crypto_msg_parser::parse_trade(&exchange, market_type, &json).unwrap(), ) .unwrap(), MessageType::L2Event => { let received_at = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() .as_millis() .try_into() .unwrap(); serde_json::to_string( &crypto_msg_parser::parse_l2( &exchange, market_type, &json, Some(received_at), ) .unwrap(), ) .unwrap() } _ => panic!("unknown msg type {msg_type}"), }; if tx.send(parsed).is_err() { break; // break the loop if there is no receiver } } }); tx_raw } async fn crawl_event_one_chunk( exchange: String, market_type: MarketType, msg_type: MessageType, ws_client: Option>, symbols: Vec, tx: Sender, ) -> tokio::task::JoinHandle<()> { let ws_client = if let Some(ws_client) = ws_client { ws_client } else { let tx_clone = tx.clone(); create_ws_client(&exchange, market_type, msg_type, tx_clone).await }; { // fire and forget let exchange_clone = exchange.to_string(); let ws_client_clone = ws_client.clone(); tokio::task::spawn(async move { subscribe_with_lock(exchange_clone, market_type, msg_type, symbols, ws_client_clone) .await; }); } tokio::task::spawn(async move { ws_client.run().await; ws_client.close().await; }) } pub(crate) async fn crawl_event( exchange: &str, msg_type: MessageType, market_type: MarketType, symbols: Option<&[String]>, tx: Sender, ) { let num_topics_per_connection = get_num_subscriptions_per_connection(exchange, market_type); let is_empty = match symbols { Some(list) => { if list.is_empty() { true } else { tokio::task::block_in_place(move || check_args(exchange, market_type, list)); false } } None => true, }; let automatic_symbol_discovery = is_empty; let real_symbols = if is_empty { tokio::task::block_in_place(move || fetch_symbols_retry(exchange, market_type)) } else { symbols.unwrap().to_vec() }; if real_symbols.is_empty() { error!("real_symbols is empty due to fetch_symbols_retry() failure"); return; } // The stop channel is used by all tokio tasks let (stop_ch_tx, stop_ch_rx) = tokio::sync::broadcast::channel::(1); // create a thread to discover new symbols let (tx_symbols, rx_symbols) = tokio::sync::mpsc::channel::>(4); let symbol_discovery_thread = if automatic_symbol_discovery { let thread = create_symbol_discovery_thread( exchange.to_string(), market_type, real_symbols.clone(), stop_ch_rx, tx_symbols, ); Some(thread) } else { None }; // create a thread to convert Sender to Sender if real_symbols.len() <= num_topics_per_connection { let ws_client = create_ws_client(exchange, market_type, msg_type, tx).await; subscribe_with_lock( exchange.to_string(), market_type, msg_type, real_symbols, ws_client.clone(), ) .await; if automatic_symbol_discovery { create_new_symbol_receiver_thread( exchange.to_string(), msg_type, market_type, rx_symbols, ws_client.clone(), ); } ws_client.run().await; ws_client.close().await; } else { // split to chunks let mut chunks: Vec> = Vec::new(); for i in (0..real_symbols.len()).step_by(num_topics_per_connection) { let chunk = real_symbols [i..(std::cmp::min(i + num_topics_per_connection, real_symbols.len()))] .to_vec(); chunks.push(chunk); } debug!("{} {} {}", real_symbols.len(), num_topics_per_connection, chunks.len(),); assert!(chunks.len() > 1); let mut last_ws_client = None; let mut handles = Vec::new(); { let n = chunks.len(); for (i, chunk) in chunks.into_iter().enumerate() { last_ws_client = if i == (n - 1) { let tx_clone = tx.clone(); Some(create_ws_client(exchange, market_type, msg_type, tx_clone).await) } else { None }; let ret = crawl_event_one_chunk( exchange.to_string(), market_type, msg_type, last_ws_client.clone(), chunk, tx.clone(), ); handles.push(ret.await); } drop(tx); } if automatic_symbol_discovery && last_ws_client.is_some() { create_new_symbol_receiver_thread( exchange.to_string(), msg_type, market_type, rx_symbols, last_ws_client.unwrap(), ); } for handle in handles { if let Err(err) = handle.await { panic!("{}", err); // TODO: use tokio::task::JoinSet or futures::stream::FuturesUnordered } } }; _ = stop_ch_tx.send(EmptyStruct {}); if let Some(thread) = symbol_discovery_thread { _ = thread.await; } } // from 1m to 5m fn get_candlestick_intervals(exchange: &str, market_type: MarketType) -> Vec { match exchange { "binance" => vec![60, 180, 300], "bybit" => vec![60, 180, 300], "deribit" => vec![60, 180, 300], "gate" => vec![10, 60, 300], "kucoin" => match market_type { MarketType::Spot => vec![60, 300], // Reduced to avoid Broken pipe (os error 32) _ => vec![60, 300], }, "okx" => vec![60, 180, 300], "zb" => match market_type { MarketType::Spot => vec![60, 180, 300], MarketType::LinearSwap => vec![60, 300], _ => vec![60, 180, 300], }, "zbg" => match market_type { MarketType::Spot => vec![60, 300], _ => vec![60, 180, 300], }, _ => vec![60, 300], } } async fn crawl_candlestick_one_chunk( exchange: String, market_type: MarketType, ws_client: Option>, symbol_interval_list: Vec<(String, usize)>, tx: Sender, ) -> tokio::task::JoinHandle<()> { let ws_client = if let Some(ws_client) = ws_client { ws_client } else { let tx_clone = tx.clone(); create_ws_client(&exchange, market_type, MessageType::Candlestick, tx_clone).await }; { // fire and forget let ws_client_clone = ws_client.clone(); tokio::task::spawn(async move { ws_client_clone.subscribe_candlestick(&symbol_interval_list).await; }); } tokio::task::spawn(async move { ws_client.run().await; ws_client.close().await; }) } pub(crate) async fn crawl_candlestick_ext( exchange: &str, market_type: MarketType, symbol_interval_list: Option<&[(String, usize)]>, tx: Sender, ) { let num_topics_per_connection = get_num_subscriptions_per_connection(exchange, market_type); let is_empty = match symbol_interval_list { Some(list) => { if list.is_empty() { true } else { let symbols: Vec = list.iter().map(|t| t.0.clone()).collect(); tokio::task::block_in_place(move || check_args(exchange, market_type, &symbols)); false } } None => true, }; let automatic_symbol_discovery = is_empty; let symbol_interval_list: Vec<(String, usize)> = if is_empty { let symbols = tokio::task::block_in_place(move || fetch_symbols_retry(exchange, market_type)); let intervals = get_candlestick_intervals(exchange, market_type); symbols .iter() .flat_map(|symbol| { intervals.clone().into_iter().map(move |interval| (symbol.clone(), interval)) }) .collect::>() } else { symbol_interval_list.unwrap().to_vec() }; if symbol_interval_list.is_empty() { error!("symbol_interval_list is empty due to fetch_symbols_retry() failure"); return; } let real_symbols: Vec = symbol_interval_list.iter().map(|t| t.0.clone()).collect(); let real_intervals: Vec = symbol_interval_list.iter().map(|t| t.1).collect(); // The stop channel is used by all tokio tasks let (stop_ch_tx, stop_ch_rx) = tokio::sync::broadcast::channel::(1); // create a thread to discover new symbols let (tx_symbols, rx_symbols) = tokio::sync::mpsc::channel::>(4); let symbol_discovery_thread = if automatic_symbol_discovery { let thread = create_symbol_discovery_thread( exchange.to_string(), market_type, real_symbols, stop_ch_rx, tx_symbols, ); Some(thread) } else { None }; if symbol_interval_list.len() <= num_topics_per_connection { let ws_client = create_ws_client(exchange, market_type, MessageType::Candlestick, tx).await; ws_client.subscribe_candlestick(&symbol_interval_list).await; if automatic_symbol_discovery { create_new_symbol_receiver_thread_candlestick( real_intervals, rx_symbols, ws_client.clone(), ); } ws_client.run().await; ws_client.close().await; } else { // split to chunks let mut chunks: Vec> = Vec::new(); { for i in (0..symbol_interval_list.len()).step_by(num_topics_per_connection) { let chunk: Vec<(String, usize)> = symbol_interval_list [i..(std::cmp::min(i + num_topics_per_connection, symbol_interval_list.len()))] .to_vec(); chunks.push(chunk); } } debug!("{} {} {}", symbol_interval_list.len(), num_topics_per_connection, chunks.len(),); assert!(chunks.len() > 1); let mut last_ws_client = None; let mut handles = Vec::new(); { let n = chunks.len(); for (i, chunk) in chunks.into_iter().enumerate() { last_ws_client = if i == (n - 1) { let tx_clone = tx.clone(); Some( create_ws_client(exchange, market_type, MessageType::Candlestick, tx_clone) .await, ) } else { None }; let ret = crawl_candlestick_one_chunk( exchange.to_string(), market_type, last_ws_client.clone(), chunk, tx.clone(), ); handles.push(ret.await); } drop(tx); } if automatic_symbol_discovery && last_ws_client.is_some() { create_new_symbol_receiver_thread_candlestick( real_intervals, rx_symbols, last_ws_client.unwrap(), ); } for handle in handles { if let Err(err) = handle.await { panic!("{}", err); // TODO: use tokio::task::JoinSet or futures::stream::FuturesUnordered } } }; _ = stop_ch_tx.send(EmptyStruct {}); if let Some(thread) = symbol_discovery_thread { _ = thread.await; } } ================================================ FILE: crypto-crawler/src/crawlers/zb.rs ================================================ use std::sync::mpsc::Sender; use crate::{crawlers::utils::crawl_event, msg::Message}; use crypto_market_type::MarketType; use crypto_msg_type::MessageType; use crypto_ws_client::*; use super::utils::create_conversion_thread; const EXCHANGE_NAME: &str = "zb"; pub(crate) async fn crawl_ticker( market_type: MarketType, symbols: Option<&[String]>, tx: Sender, ) { if market_type == MarketType::LinearSwap && (symbols.is_none() || symbols.unwrap().is_empty()) { let tx = create_conversion_thread( EXCHANGE_NAME.to_string(), MessageType::Ticker, market_type, tx, ); let commands: Vec = vec![r#"{"action": "subscribe","channel": "All.Ticker"}"#.to_string()]; let ws_client = ZbSwapWSClient::new(tx, None).await; ws_client.send(&commands).await; ws_client.run().await; ws_client.close().await; } else { crawl_event(EXCHANGE_NAME, MessageType::Ticker, market_type, symbols, tx).await; } } ================================================ FILE: crypto-crawler/src/crawlers/zbg.rs ================================================ use std::sync::mpsc::Sender; use crate::{crawlers::utils::crawl_event, msg::Message}; use crypto_market_type::MarketType; use crypto_msg_type::MessageType; use crypto_ws_client::*; use super::utils::create_conversion_thread; const EXCHANGE_NAME: &str = "zbg"; pub(crate) async fn crawl_ticker( market_type: MarketType, symbols: Option<&[String]>, tx: Sender, ) { if symbols.is_none() || symbols.unwrap().is_empty() { if market_type == MarketType::Spot { let tx = create_conversion_thread( EXCHANGE_NAME.to_string(), MessageType::Ticker, market_type, tx, ); let commands: Vec = vec![r#"{"action":"ADD", "dataType":"ALL_TRADE_STATISTIC_24H"}"#.to_string()]; let ws_client = ZbgSpotWSClient::new(tx, None).await; ws_client.send(&commands).await; ws_client.run().await; ws_client.close().await; } else { let tx = create_conversion_thread( EXCHANGE_NAME.to_string(), MessageType::Ticker, market_type, tx, ); let commands: Vec = vec![r#"{"action":"sub", "topic":"future_all_indicator"}"#.to_string()]; let ws_client = ZbgSwapWSClient::new(tx, None).await; ws_client.send(&commands).await; ws_client.run().await; ws_client.close().await; } } else { crawl_event(EXCHANGE_NAME, MessageType::Ticker, market_type, symbols, tx).await; } } ================================================ FILE: crypto-crawler/src/lib.rs ================================================ //! A rock-solid cryprocurrency crawler. //! //! ## Crawl realtime trades //! //! ```rust //! use crypto_crawler::{crawl_trade, MarketType}; //! //! #[tokio::main(flavor = "multi_thread")] //! async fn main() { //! let (tx, rx) = std::sync::mpsc::channel(); //! tokio::task::spawn(async move { //! // Crawl realtime trades for all symbols of binance inverse_swap markets //! crawl_trade("binance", MarketType::InverseSwap, None, tx).await; //! }); //! //! let mut messages = Vec::new(); //! for msg in rx { //! messages.push(msg); //! break; //! } //! assert!(!messages.is_empty()); //! } //! ``` //! //! ## Crawl realtime level2 orderbook incremental updates //! //! ```rust //! use crypto_crawler::{crawl_l2_event, MarketType}; //! //! #[tokio::main(flavor = "multi_thread")] //! async fn main() { //! let (tx, rx) = std::sync::mpsc::channel(); //! tokio::task::spawn(async move { //! // Crawl realtime level2 incremental updates for all symbols of binance inverse_swap markets //! crawl_l2_event("binance", MarketType::InverseSwap, None, tx).await; //! }); //! //! let mut messages = Vec::new(); //! for msg in rx { //! messages.push(msg); //! break; //! } //! assert!(!messages.is_empty()); //! } //! ``` //! //! ## Crawl level2 orderbook full snapshots from RESTful API //! //! ```rust //! use crypto_crawler::{crawl_l2_snapshot, MarketType}; //! //! let (tx, rx) = std::sync::mpsc::channel(); //! std::thread::spawn(move || { //! // Crawl level2 full snapshots for all symbols of binance inverse_swap markets //! crawl_l2_snapshot("binance", MarketType::InverseSwap, None, tx); //! }); //! //! let mut messages = Vec::new(); //! for msg in rx { //! messages.push(msg); //! break; //! } //! assert!(!messages.is_empty()); //! ``` //! //! ## Crawl realtime level2 orderbook top-K snapshots //! //! ```rust //! use crypto_crawler::{crawl_l2_topk, MarketType}; //! //! #[tokio::main(flavor = "multi_thread")] //! async fn main() { //! let (tx, rx) = std::sync::mpsc::channel(); //! tokio::task::spawn(async move { //! // Crawl realtime level2 top-k snapshots for all symbols of binance inverse_swap markets //! crawl_l2_topk("binance", MarketType::InverseSwap, None, tx).await; //! }); //! //! let mut messages = Vec::new(); //! for msg in rx { //! messages.push(msg); //! break; //! } //! assert!(!messages.is_empty()); //! } //! ``` //! //! ## Crawl realtime level3 orderbook incremental updates //! //! ```rust //! use crypto_crawler::{crawl_l3_event, MarketType}; //! //! #[tokio::main(flavor = "multi_thread")] //! async fn main() { //! let (tx, rx) = std::sync::mpsc::channel(); //! tokio::task::spawn(async move { //! // Crawl realtime level3 updates for all symbols of CoinbasePro spot market //! crawl_l3_event("coinbase_pro", MarketType::Spot, None, tx).await; //! }); //! //! let mut messages = Vec::new(); //! for msg in rx { //! messages.push(msg); //! break; //! } //! assert!(!messages.is_empty()); //! } //! ``` //! //! ## Crawl level3 orderbook full snapshots from RESTful API //! //! ```rust //! use crypto_crawler::{crawl_l3_snapshot, MarketType}; //! //! let (tx, rx) = std::sync::mpsc::channel(); //! std::thread::spawn(move || { //! // Crawl level3 orderbook full snapshots for all symbols of CoinbasePro spot markets //! crawl_l3_snapshot("coinbase_pro", MarketType::Spot, None, tx); //! }); //! //! let mut messages = Vec::new(); //! for msg in rx { //! messages.push(msg); //! break; //! } //! assert!(!messages.is_empty()); //! ``` //! //! ## Crawl realtime BBO //! //! ```rust //! use crypto_crawler::{crawl_bbo, MarketType}; //! //! #[tokio::main(flavor = "multi_thread")] //! async fn main() { //! let (tx, rx) = std::sync::mpsc::channel(); //! tokio::task::spawn(async move { //! // Crawl realtime best bid and ask messages for all symbols of binance COIN-margined perpetual markets //! crawl_bbo("binance", MarketType::InverseSwap, None, tx).await; //! }); //! //! let mut messages = Vec::new(); //! for msg in rx { //! messages.push(msg); //! break; //! } //! assert!(!messages.is_empty()); //! } //! ``` //! //! ## Crawl 24hr rolling window tickers //! //! ```rust //! use crypto_crawler::{crawl_ticker, MarketType}; //! //! #[tokio::main(flavor = "multi_thread")] //! async fn main() { //! let (tx, rx) = std::sync::mpsc::channel(); //! tokio::task::spawn(async move { //! // Crawl 24hr rolling window tickers for all symbols of binance COIN-margined perpetual markets //! crawl_ticker("binance", MarketType::InverseSwap, None, tx).await; //! }); //! //! let mut messages = Vec::new(); //! for msg in rx { //! messages.push(msg); //! break; //! } //! assert!(!messages.is_empty()); //! } //! ``` //! //! ## Crawl candlesticks(i.e., OHLCV) //! //! ```rust //! use crypto_crawler::{crawl_candlestick, MarketType}; //! //! #[tokio::main(flavor = "multi_thread")] //! async fn main() { //! let (tx, rx) = std::sync::mpsc::channel(); //! tokio::task::spawn(async move { //! // Crawl candlesticks from 1 minute to 3 minutes for all symbols of binance COIN-margined perpetual markets //! crawl_candlestick("binance", MarketType::InverseSwap, None, tx).await; //! }); //! //! let mut messages = Vec::new(); //! for msg in rx { //! messages.push(msg); //! break; //! } //! assert!(!messages.is_empty()); //! } //! ``` //! //! ## Crawl funding rates //! //! ```rust //! use crypto_crawler::{crawl_funding_rate, MarketType}; //! //! #[tokio::main(flavor = "multi_thread")] //! async fn main() { //! let (tx, rx) = std::sync::mpsc::channel(); //! tokio::task::spawn(async move { //! // Crawl funding rates for all symbols of binance COIN-margined perpetual markets //! crawl_funding_rate("binance", MarketType::InverseSwap, None, tx).await; //! }); //! //! let mut messages = Vec::new(); //! for msg in rx { //! messages.push(msg); //! break; //! } //! assert!(!messages.is_empty()); //! } //! ``` mod crawlers; mod msg; mod utils; use std::sync::mpsc::Sender; pub use crawlers::fetch_symbols_retry; pub use crypto_market_type::MarketType; pub use crypto_msg_type::MessageType; pub use msg::*; pub use utils::get_hot_spot_symbols; /// Crawl realtime trades. /// /// If `symbols` is None or empty, this API will crawl realtime trades for all /// symbols in the `market_type` market, and launch a thread to discover new /// symbols every hour. And so forth for all other APIs. pub async fn crawl_trade( exchange: &str, market_type: MarketType, symbols: Option<&[String]>, tx: Sender, ) { match exchange { "binance" => crawlers::binance::crawl_trade(market_type, symbols, tx).await, "bitmex" => crawlers::bitmex::crawl_trade(market_type, symbols, tx).await, "deribit" => crawlers::deribit::crawl_trade(market_type, symbols, tx).await, "bitfinex" | "bitget" | "bithumb" | "bitstamp" | "bitz" | "bybit" | "coinbase_pro" | "dydx" | "ftx" | "gate" | "huobi" | "kraken" | "kucoin" | "mexc" | "okx" | "zb" | "zbg" => { crawlers::crawl_event(exchange, MessageType::Trade, market_type, symbols, tx).await } _ => panic!("{exchange} does NOT have the trade websocket channel"), } } /// Crawl level2 orderbook update events. pub async fn crawl_l2_event( exchange: &str, market_type: MarketType, symbols: Option<&[String]>, tx: Sender, ) { match exchange { "bitmex" => crawlers::bitmex::crawl_l2_event(market_type, symbols, tx).await, "huobi" => crawlers::huobi::crawl_l2_event(market_type, symbols, tx).await, "binance" | "bitfinex" | "bitget" | "bithumb" | "bitstamp" | "bitz" | "bybit" | "coinbase_pro" | "deribit" | "dydx" | "ftx" | "gate" | "kraken" | "kucoin" | "mexc" | "okx" | "zb" | "zbg" => { crawlers::crawl_event(exchange, MessageType::L2Event, market_type, symbols, tx).await } _ => panic!("{exchange} does NOT have the incremental level2 websocket channel"), } } /// Crawl level3 orderbook update events. pub async fn crawl_l3_event( exchange: &str, market_type: MarketType, symbols: Option<&[String]>, tx: Sender, ) { match exchange { "bitfinex" | "bitstamp" | "coinbase_pro" | "kucoin" => { crawlers::crawl_event(exchange, MessageType::L3Event, market_type, symbols, tx).await } _ => panic!("{exchange} does NOT have the incremental level3 websocket channel"), } } /// Crawl level2 orderbook snapshots through RESTful APIs. pub fn crawl_l2_snapshot( exchange: &str, market_type: MarketType, symbols: Option<&[String]>, tx: Sender, ) { crawlers::crawl_snapshot(exchange, market_type, MessageType::L2Snapshot, symbols, tx); } /// Crawl best bid and ask. pub async fn crawl_bbo( exchange: &str, market_type: MarketType, symbols: Option<&[String]>, tx: Sender, ) { match exchange { "binance" => crawlers::binance::crawl_bbo(market_type, symbols, tx).await, "bitmex" => crawlers::bitmex::crawl_bbo(market_type, symbols, tx).await, "kucoin" => crawlers::kucoin::crawl_bbo(market_type, symbols, tx).await, "deribit" | "ftx" | "gate" | "huobi" | "kraken" | "okx" => { crawlers::crawl_event(exchange, MessageType::BBO, market_type, symbols, tx).await } _ => panic!("{exchange} does NOT have BBO websocket channel"), } } /// Crawl level2 orderbook top-k snapshots through websocket. pub async fn crawl_l2_topk( exchange: &str, market_type: MarketType, symbols: Option<&[String]>, tx: Sender, ) { match exchange { "bitmex" => crawlers::bitmex::crawl_l2_topk(market_type, symbols, tx).await, "binance" | "bitget" | "bybit" | "bitstamp" | "deribit" | "gate" | "huobi" | "kucoin" | "mexc" | "okx" | "zb" => { crawlers::crawl_event(exchange, MessageType::L2TopK, market_type, symbols, tx).await } _ => panic!("{exchange} does NOT have the level2 top-k snapshot websocket channel"), } } /// Crawl level3 orderbook snapshots through RESTful APIs. pub fn crawl_l3_snapshot( exchange: &str, market_type: MarketType, symbols: Option<&[String]>, tx: Sender, ) { crawlers::crawl_snapshot(exchange, market_type, MessageType::L3Snapshot, symbols, tx) } /// Crawl 24hr rolling window ticker. /// /// If `symbols` is None, it means all trading symbols in the `market_type`, /// and updates the latest symbols every hour. pub async fn crawl_ticker( exchange: &str, market_type: MarketType, symbols: Option<&[String]>, tx: Sender, ) { match exchange { "binance" => crawlers::binance::crawl_ticker(market_type, symbols, tx).await, "bitfinex" | "bitget" | "bithumb" | "bitz" | "bybit" | "coinbase_pro" | "deribit" | "gate" | "huobi" | "kraken" | "kucoin" | "mexc" | "okx" => { crawlers::crawl_event(exchange, MessageType::Ticker, market_type, symbols, tx).await } "zb" => crawlers::zb::crawl_ticker(market_type, symbols, tx).await, "zbg" => crawlers::zbg::crawl_ticker(market_type, symbols, tx).await, _ => panic!("{exchange} does NOT have the ticker websocket channel"), } } /// Crawl perpetual swap funding rates. pub async fn crawl_funding_rate( exchange: &str, market_type: MarketType, symbols: Option<&[String]>, tx: Sender, ) { match exchange { "binance" => crawlers::binance::crawl_funding_rate(market_type, symbols, tx).await, "bitmex" => crawlers::bitmex::crawl_funding_rate(market_type, symbols, tx).await, "huobi" => crawlers::huobi::crawl_funding_rate(market_type, symbols, tx).await, "okx" => crawlers::okx::crawl_funding_rate(market_type, symbols, tx).await, _ => panic!("{exchange} does NOT have perpetual swap market"), } } /// Crawl candlestick(i.e., OHLCV) data. /// /// If `symbol_interval_list` is None or empty, this API will crawl candlesticks /// from 10 seconds to 3 minutes(if available) for all symbols. pub async fn crawl_candlestick( exchange: &str, market_type: MarketType, symbol_interval_list: Option<&[(String, usize)]>, tx: Sender, ) { match exchange { "bitmex" => { crawlers::bitmex::crawl_candlestick(market_type, symbol_interval_list, tx).await } "binance" | "bitfinex" | "bitget" | "bitz" | "bybit" | "deribit" | "gate" | "huobi" | "kraken" | "kucoin" | "mexc" | "okx" | "zb" | "zbg" => { crawlers::crawl_candlestick_ext(exchange, market_type, symbol_interval_list, tx).await } _ => panic!("{exchange} does NOT have the candlestick websocket channel"), }; } /// Crawl all open interest. pub fn crawl_open_interest(exchange: &str, market_type: MarketType, tx: Sender) { crawlers::crawl_open_interest(exchange, market_type, tx); } /// Subscribe to multiple message types of one symbol. /// /// This API is suitable for client applications such as APP, website, etc. /// /// String messages in `tx` are already parsed by `crypto-msg-parser`. pub async fn subscribe_symbol( exchange: &str, market_type: MarketType, symbol: &str, msg_types: &[MessageType], tx: Sender, ) { let ws_client = crawlers::create_ws_client_symbol(exchange, market_type, tx).await; let symbols = vec![symbol.to_string()]; let commands = crypto_msg_type::get_ws_commands(exchange, msg_types, &symbols, true, None); ws_client.send(&commands).await; ws_client.run().await; ws_client.close().await; } ================================================ FILE: crypto-crawler/src/msg.rs ================================================ use crypto_market_type::MarketType; use crypto_msg_type::MessageType; use serde::{Deserialize, Serialize}; use std::{ convert::TryInto, str::FromStr, time::{SystemTime, UNIX_EPOCH}, }; /// Message represents messages received by crawlers. #[derive(Serialize, Deserialize)] pub struct Message { /// The exchange name, unique for each exchage pub exchange: String, /// Market type pub market_type: MarketType, /// Message type pub msg_type: MessageType, #[serde(skip_serializing_if = "Option::is_none")] pub symbol: Option, /// Unix timestamp in milliseconds pub received_at: u64, /// the original message pub json: String, } impl Message { pub fn new( exchange: String, market_type: MarketType, msg_type: MessageType, json: String, ) -> Self { Message { exchange, market_type, msg_type, symbol: None, received_at: SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() .as_millis() .try_into() .unwrap(), json: json.trim().to_string(), } } pub fn new_with_symbol( exchange: String, market_type: MarketType, msg_type: MessageType, symbol: String, json: String, ) -> Self { let mut msg = Self::new(exchange, market_type, msg_type, json); msg.symbol = Some(symbol); msg } /// Convert to a TSV string. /// /// The `exchange`, `market_type` and `msg_type` fields are not included to /// save some disk space. pub fn to_tsv_string(&self) -> String { let symbol = if let Some(symbol) = self.symbol.clone() { symbol } else { "".to_string() }; format!("{}\t{}\t{}", self.received_at, symbol, self.json) } /// Convert from a TSV string. pub fn from_tsv_string(exchange: &str, market_type: &str, msg_type: &str, s: &str) -> Self { let v: Vec<&str> = s.split('\t').collect(); assert_eq!(3, v.len()); let market_type = MarketType::from_str(market_type).unwrap(); let msg_type = MessageType::from_str(msg_type).unwrap(); let symbol = if v[1].is_empty() { None } else { Some(v[1].to_string()) }; Message { exchange: exchange.to_string(), market_type, msg_type, symbol, received_at: v[0].parse::().unwrap(), json: v[2].to_string(), } } } impl std::fmt::Display for Message { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", serde_json::to_string(self).unwrap()) } } ================================================ FILE: crypto-crawler/src/utils/cmc_rank.rs ================================================ use reqwest::header; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; use once_cell::sync::Lazy; pub(super) static CMC_RANKS: Lazy> = Lazy::new(|| { // offline data, in case the network is down let offline: HashMap = vec![ ("BTC", 1), ("ETH", 2), ("USDT", 3), ("BNB", 4), ("USDC", 5), ("XRP", 6), ("ADA", 7), ("BUSD", 8), ("MATIC", 9), ("DOGE", 10), ("SOL", 11), ("DOT", 12), ("SHIB", 13), ("LTC", 14), ("TRX", 15), ("AVAX", 16), ("DAI", 17), ("UNI", 18), ("WBTC", 19), ("LINK", 20), ("ATOM", 21), ("LEO", 22), ("OKB", 23), ("ETC", 24), ("TON", 25), ("XMR", 26), ("FIL", 27), ("BCH", 28), ("LDO", 29), ("XLM", 30), ("APT", 31), ("CRO", 32), ("NEAR", 33), ("VET", 34), ("HBAR", 35), ("APE", 36), ("ALGO", 37), ("ICP", 38), ("QNT", 39), ("GRT", 40), ("FTM", 41), ("FLOW", 42), ("EGLD", 43), ("MANA", 44), ("THETA", 45), ("EOS", 46), ("BIT", 47), ("AAVE", 48), ("XTZ", 49), ("AXS", 50), ("SAND", 51), ("STX", 52), ("TUSD", 53), ("LUNC", 54), ("RPL", 55), ("KLAY", 56), ("CHZ", 57), ("USDP", 58), ("NEO", 59), ("HT", 60), ("KCS", 61), ("BSV", 62), ("IMX", 63), ("MINA", 64), ("DASH", 65), ("CAKE", 66), ("FXS", 67), ("MKR", 68), ("CRV", 69), ("ZEC", 70), ("USDD", 71), ("MIOTA", 72), ("OP", 73), ("XEC", 74), ("BTT", 75), ("SNX", 76), ("GMX", 77), ("GUSD", 78), ("CFX", 79), ("GT", 80), ("TWT", 81), ("RUNE", 82), ("ZIL", 83), ("PAXG", 84), ("AGIX", 85), ("LRC", 86), ("ENJ", 87), ("OSMO", 88), ("1INCH", 89), ("FLR", 90), ("DYDX", 91), ("BAT", 92), ("SSV", 94), ("BONE", 95), ("CVX", 96), ("FEI", 97), ("ANKR", 98), ("CSPR", 99), ("ETHW", 100), ("BNX", 101), ("NEXO", 102), ("ROSE", 103), ("RVN", 104), ("LUNA", 105), ("CELO", 106), ("HNT", 107), ("COMP", 108), ("TFUEL", 109), ("XEM", 110), ("XDC", 111), ("KAVA", 112), ("RNDR", 113), ("HOT", 114), ("WOO", 115), ("YFI", 116), ("FET", 117), ("QTUM", 118), ("MOB", 119), ("DCR", 120), ("MAGIC", 121), ("T", 122), ("AR", 123), ("BLUR", 124), ("AUDIO", 125), ("KSM", 126), ("BAL", 127), ("ASTR", 128), ("ENS", 129), ("BTG", 130), ("SUSHI", 131), ("JASMY", 132), ("ONE", 133), ("GALA", 134), ("WAVES", 135), ("GNO", 136), ("USTC", 137), ("GLM", 138), ("IOTX", 139), ("INJ", 140), ("JST", 141), ("GLMR", 142), ("XCH", 143), ("MASK", 144), ("BAND", 145), ("AMP", 146), ("KDA", 147), ("OCEAN", 148), ("ICX", 149), ("OMG", 150), ("RSR", 151), ("ELON", 152), ("SC", 153), ("FLUX", 154), ("GMT", 155), ("ZRX", 156), ("CHSB", 157), ("ONT", 158), ("BICO", 159), ("XCN", 160), ("IOST", 161), ("XYM", 162), ("HIVE", 163), ("DAO", 164), ("LPT", 165), ("SKL", 166), ("ACH", 167), ("CKB", 168), ("SYN", 169), ("BORA", 170), ("WAXP", 171), ("SFP", 172), ("DGB", 173), ("STORJ", 174), ("SXP", 175), ("POLY", 176), ("EVER", 177), ("STG", 178), ("ZEN", 179), ("ILV", 180), ("KEEP", 181), ("ELF", 182), ("RLC", 183), ("LSK", 184), ("UMA", 185), ("KNC", 186), ("METIS", 187), ("CELR", 188), ("MC", 189), ("SLP", 190), ("PUNDIX", 191), ("BTRST", 192), ("RIF", 193), ("TRAC", 194), ("PLA", 195), ("EWT", 196), ("MED", 197), ("SYS", 198), ("SCRT", 199), ("NFT", 200), ("HEX", 201), ("WTRX", 202), ("stETH", 203), ("BTCB", 204), ("TMG", 205), ("WBNB", 206), ("FRAX", 207), ("HBTC", 208), ("BTTOLD", 209), ("TNC", 210), ("WEMIX", 211), ("BGB", 212), ("FTT", 213), ("XRD", 214), ("XAUT", 215), ("FLOKI", 216), ("NXM", 217), ("BabyDoge", 218), ("USDJ", 219), ("ASTRAFER", 220), ("LN", 221), ("DFI", 222), ("BRISE", 223), ("MV", 224), ("LUSD", 225), ("EDGT", 226), ("ANY", 227), ("ALI", 228), ("WEVER", 229), ("COCOS", 230), ("TEL", 231), ("LYXe", 232), ("MULTI", 233), ("KAS", 234), ("BDX", 235), ("CORE", 236), ("RON", 237), ("VVS", 238), ("PEOPLE", 239), ("HFT", 240), ("EURS", 241), ("MX", 242), ("API3", 243), ("AXL", 244), ("VGX", 245), ("GTC", 246), ("RBN", 247), ("CHR", 248), ("XNO", 249), ("DENT", 250), ("CVC", 251), ("LQTY", 252), ("CEL", 253), ("POLYX", 254), ("HOOK", 255), ("XTN", 256), ] .into_iter() .map(|x| (x.0.to_string(), x.1)) .collect(); let online = get_cmc_ranks(1024); if online.is_empty() { offline } else { online } }); fn http_get(url: &str) -> Result { let mut headers = header::HeaderMap::new(); headers.insert(header::CONTENT_TYPE, header::HeaderValue::from_static("application/json")); let client = reqwest::blocking::Client::builder() .default_headers(headers) .user_agent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36") .gzip(true) .build()?; let response = client.get(url).send()?; match response.error_for_status() { Ok(resp) => Ok(resp.text()?), Err(error) => Err(error), } } // Returns a map of coin to cmcRank. fn get_cmc_ranks(limit: i64) -> HashMap { let mut mapping: HashMap = HashMap::new(); let url = format!( "https://api.coinmarketcap.com/data-api/v3/cryptocurrency/listing?start=1&limit={limit}&sortBy=market_cap&sortType=desc&convert=USD&cryptoType=all&tagType=all&audited=false" ); if let Ok(txt) = http_get(&url) { if let Ok(json_obj) = serde_json::from_str::>(&txt) { if let Some(data) = json_obj.get("data") { #[derive(Serialize, Deserialize)] #[allow(non_snake_case)] struct Currency { id: i64, name: String, symbol: String, cmcRank: u64, } let arr = data["cryptoCurrencyList"].as_array().unwrap(); for currency in arr { let currency: Currency = serde_json::from_value(currency.clone()).unwrap(); mapping.insert(currency.symbol, currency.cmcRank); } } } } mapping } pub(crate) fn sort_by_cmc_rank(exchange: &str, symbols: &mut [String]) { symbols.sort_by_key(|symbol| { if let Some(pair) = crypto_pair::normalize_pair(symbol, exchange) { let base = pair.split('/').next().unwrap(); *CMC_RANKS.get(base).unwrap_or(&u64::max_value()) } else { u64::max_value() } }); } #[cfg(test)] mod tests { use crypto_market_type::MarketType; #[test] fn test_get_cmc_ranks() { let mapping = super::get_cmc_ranks(256); let mut v = Vec::from_iter(mapping); v.sort_by(|&(_, a), &(_, b)| a.cmp(&b)); for (coin, rank) in v { println!("(\"{coin}\", {rank}),"); } } #[test] fn test_sort_by_cmc_rank() { let mut binance_linear_swap = crypto_markets::fetch_symbols("binance", MarketType::LinearSwap).unwrap(); super::sort_by_cmc_rank("binance", &mut binance_linear_swap); assert_eq!("BTCUSDT", binance_linear_swap[0]); assert_eq!("BTCBUSD", binance_linear_swap[1]); assert_eq!("ETHUSDT", binance_linear_swap[2]); assert_eq!("ETHBUSD", binance_linear_swap[3]); } } ================================================ FILE: crypto-crawler/src/utils/lock.rs ================================================ use std::{collections::HashMap, sync::Arc}; use crypto_market_type::MarketType; use fslock::LockFile; use once_cell::sync::Lazy; const EXCHANGES: &[&str] = &[ "binance", "bitfinex", "bitget", "bithumb", "bitmex", "bitstamp", "bitz", "bybit", "coinbase_pro", "deribit", "dydx", "ftx", "gate", "huobi", "kraken", "kucoin", "mexc", "okx", "zb", "zbg", ]; const EXCHANGES_WS: &[&str] = &["bitfinex", "bitz", "kucoin", "okx"]; #[allow(clippy::type_complexity)] pub(crate) static REST_LOCKS: Lazy< HashMap>>>, > = Lazy::new(create_all_lock_files_rest); #[allow(clippy::type_complexity)] pub(crate) static WS_LOCKS: Lazy< HashMap>>>, > = Lazy::new(create_all_lock_files_ws); /// Markets with the same endpoint will have the same file name. fn get_lock_file_name(exchange: &str, market_type: MarketType, prefix: &str) -> String { let filename = match exchange { "binance" => match market_type { MarketType::InverseSwap | MarketType::InverseFuture => { "binance_inverse.lock".to_string() } MarketType::LinearSwap | MarketType::LinearFuture => "binance_linear.lock".to_string(), MarketType::Spot => "binance_spot.lock".to_string(), MarketType::EuropeanOption => "binance_option.lock".to_string(), _ => panic!("Unknown market_type {market_type} of {exchange}"), }, "bitfinex" => "bitfinex.lock".to_string(), "bitget" => match market_type { MarketType::InverseFuture | MarketType::InverseSwap | MarketType::LinearSwap => { "bitget_swap.lock".to_string() } MarketType::Spot => "bitget_spot.lock".to_string(), _ => panic!("Unknown market_type {market_type} of {exchange}"), }, "bitmex" => "bitmex.lock".to_string(), "bitz" => match market_type { MarketType::InverseSwap | MarketType::LinearSwap => "bitz_swap.lock".to_string(), MarketType::Spot => "bitz_spot.lock".to_string(), _ => panic!("Unknown market_type {market_type} of {exchange}"), }, "bybit" => { if prefix == "rest" { "bybit.lock".to_string() } else { match market_type { MarketType::InverseSwap | MarketType::InverseFuture => { "bybit_inverse.lock".to_string() } MarketType::LinearSwap => "bybit_linear.lock".to_string(), _ => panic!("Unknown market_type {market_type} of {exchange}"), } } } "deribit" => "deribit.lock".to_string(), "ftx" => "ftx.lock".to_string(), "gate" => match market_type { MarketType::InverseSwap | MarketType::LinearSwap => "gate_swap.lock".to_string(), MarketType::InverseFuture | MarketType::LinearFuture => "gate_future.lock".to_string(), MarketType::Spot => "gate_spot.lock".to_string(), _ => panic!("Unknown market_type {market_type} of {exchange}"), }, "kucoin" => { if prefix == "ws" { "kucoin.lock".to_string() } else { match market_type { MarketType::InverseSwap | MarketType::LinearSwap | MarketType::InverseFuture => "kucoin_swap.lock".to_string(), MarketType::Spot => "kucoin_spot.lock".to_string(), MarketType::Unknown => "kucoin_unknown.lock".to_string(), // for OpenInterest _ => panic!("Unknown market_type {market_type} of {exchange}"), } } } "mexc" => match market_type { MarketType::InverseSwap | MarketType::LinearSwap => "mexc_swap.lock".to_string(), MarketType::Spot => "mexc_spot.lock".to_string(), _ => panic!("Unknown market_type {market_type} of {exchange}"), }, "okx" => "okx.lock".to_string(), "zb" => match market_type { MarketType::LinearSwap => "zb_swap.lock".to_string(), MarketType::Spot => "zb_spot.lock".to_string(), _ => panic!("Unknown market_type {market_type} of {exchange}"), }, "zbg" => match market_type { MarketType::InverseSwap | MarketType::LinearSwap => "zbg_swap.lock".to_string(), MarketType::Spot => "zbg_spot.lock".to_string(), _ => panic!("Unknown market_type {market_type} of {exchange}"), }, _ => format!("{exchange}.{market_type}.lock"), }; format!("{prefix}.{filename}") } fn create_lock_file(filename: &str) -> LockFile { let dir = if std::env::var("DATA_DIR").is_ok() { std::path::Path::new(std::env::var("DATA_DIR").unwrap().as_str()).join("locks") } else { std::env::temp_dir().join("locks") }; let _ = std::fs::create_dir_all(&dir); let file_path = dir.join(filename); LockFile::open(file_path.as_path()) .unwrap_or_else(|_| panic!("{}", file_path.to_str().unwrap().to_string())) } fn create_all_lock_files_rest() -> HashMap>>> { let prefix = "rest"; // filename -> lock let mut cache: HashMap>> = HashMap::new(); let mut result: HashMap>>> = HashMap::new(); for exchange in EXCHANGES.iter() { let m = result.entry(exchange.to_string()).or_insert_with(HashMap::new); let mut market_types = crypto_market_type::get_market_types(exchange); if *exchange == "bitmex" { market_types.push(MarketType::Unknown); } if *exchange == "deribit" { market_types.push(MarketType::Unknown); } if prefix == "rest" && (*exchange == "ftx" || *exchange == "kucoin") { market_types.push(MarketType::Unknown); // for OpenInterest } for market_type in market_types { let filename = get_lock_file_name(exchange, market_type, prefix); let lock_file = cache .entry(filename.clone()) .or_insert_with(|| Arc::new(std::sync::Mutex::new(create_lock_file(&filename)))); m.insert(market_type, lock_file.clone()); } } result } fn create_all_lock_files_ws() -> HashMap>>> { let prefix = "ws"; // filename -> lock let mut cache: HashMap>> = HashMap::new(); let mut result: HashMap>>> = HashMap::new(); for exchange in EXCHANGES_WS.iter() { let m = result.entry(exchange.to_string()).or_insert_with(HashMap::new); let mut market_types = crypto_market_type::get_market_types(exchange); if *exchange == "bitmex" { market_types.push(MarketType::Unknown); } if prefix == "rest" && (*exchange == "ftx" || *exchange == "kucoin") { market_types.push(MarketType::Unknown); // for OpenInterest } for market_type in market_types { let filename = get_lock_file_name(exchange, market_type, prefix); let lock_file = cache .entry(filename.clone()) .or_insert_with(|| Arc::new(tokio::sync::Mutex::new(create_lock_file(&filename)))); m.insert(market_type, lock_file.clone()); } } result } ================================================ FILE: crypto-crawler/src/utils/mod.rs ================================================ pub(crate) mod cmc_rank; mod lock; pub(crate) mod spot_symbols; pub(crate) use lock::{REST_LOCKS, WS_LOCKS}; pub use spot_symbols::get_hot_spot_symbols; ================================================ FILE: crypto-crawler/src/utils/spot_symbols.rs ================================================ use std::collections::HashSet; use crypto_market_type::MarketType; pub fn get_hot_spot_symbols(exchange: &str, spot_symbols: &[String]) -> Vec { let market_types = crypto_market_type::get_market_types(exchange); let cmc_ranks = &super::cmc_rank::CMC_RANKS; let contract_base_coins = { let mut contract_base_coins = HashSet::::new(); for market_type in market_types.iter().filter(|m| *m != &MarketType::Spot) { let symbols = crypto_markets::fetch_symbols(exchange, *market_type).unwrap_or_default(); for symbol in symbols { let pair = crypto_pair::normalize_pair(&symbol, exchange).unwrap(); let base_coin = pair.split('/').next().unwrap(); contract_base_coins.insert(base_coin.to_string()); } } contract_base_coins }; let is_hot = |symbol: &str| { let pair = crypto_pair::normalize_pair(symbol, exchange).unwrap(); let base_coin = pair.split('/').next().unwrap(); contract_base_coins.contains(base_coin) || *cmc_ranks.get(base_coin).unwrap_or(&u64::max_value()) <= 100 }; spot_symbols.iter().cloned().filter(|symbol| is_hot(symbol)).collect() } #[cfg(test)] mod tests { use crypto_market_type::MarketType; use super::get_hot_spot_symbols; #[test] fn test_binance() { let spot_symbols = crypto_markets::fetch_symbols("binance", MarketType::Spot).unwrap(); let symbols = get_hot_spot_symbols("binance", &spot_symbols); assert!(!symbols.is_empty()); } #[test] fn test_huobi() { let spot_symbols = crypto_markets::fetch_symbols("huobi", MarketType::Spot).unwrap(); let symbols = get_hot_spot_symbols("huobi", &spot_symbols); assert!(!symbols.is_empty()); } #[test] fn test_okx() { let spot_symbols = crypto_markets::fetch_symbols("okx", MarketType::Spot).unwrap(); let symbols = get_hot_spot_symbols("okx", &spot_symbols); assert!(!symbols.is_empty()); } } ================================================ FILE: crypto-crawler/tests/binance.rs ================================================ #[macro_use] mod utils; use test_case::test_case; use crypto_crawler::*; use crypto_market_type::MarketType; use crypto_msg_type::MessageType; use utils::parse; const EXCHANGE_NAME: &str = "binance"; #[test_case(MarketType::Spot)] #[test_case(MarketType::InverseFuture)] #[test_case(MarketType::LinearFuture)] #[test_case(MarketType::InverseSwap)] #[test_case(MarketType::LinearSwap)] #[tokio::test(flavor = "multi_thread", worker_threads = 8)] async fn test_crawl_trade_all(market_type: MarketType) { test_all_symbols!(crawl_trade, EXCHANGE_NAME, market_type, MessageType::Trade) } #[test_case(MarketType::Spot, "BTCUSDT")] #[test_case(MarketType::InverseFuture, "BTCUSD_221230")] #[test_case(MarketType::LinearFuture, "BTCUSDT_221230")] #[test_case(MarketType::InverseSwap, "BTCUSD_PERP")] #[test_case(MarketType::LinearSwap, "BTCUSDT")] // #[test_case(MarketType::EuropeanOption, "BTC-220610-30000-C"; "ignore")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_trade(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_trade, EXCHANGE_NAME, market_type, symbol, MessageType::Trade) } #[test_case(MarketType::Spot, "BTCUSDT")] #[test_case(MarketType::InverseFuture, "BTCUSD_221230")] #[test_case(MarketType::LinearFuture, "BTCUSDT_221230")] #[test_case(MarketType::InverseSwap, "BTCUSD_PERP")] #[test_case(MarketType::LinearSwap, "BTCUSDT")] // #[test_case(MarketType::EuropeanOption, "BTC-220610-30000-C"; "ignore")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_l2_event(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_l2_event, EXCHANGE_NAME, market_type, symbol, MessageType::L2Event) } #[test_case(MarketType::Spot, "BTCUSDT")] #[test_case(MarketType::InverseFuture, "BTCUSD_221230")] #[test_case(MarketType::LinearFuture, "BTCUSDT_221230")] #[test_case(MarketType::InverseSwap, "BTCUSD_PERP")] #[test_case(MarketType::LinearSwap, "BTCUSDT")] // #[test_case(MarketType::EuropeanOption, "BTC-220610-30000-C"; "ignore")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_bbo(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_bbo, EXCHANGE_NAME, market_type, symbol, MessageType::BBO) } #[test_case(MarketType::Spot, "BTCUSDT")] #[test_case(MarketType::InverseFuture, "BTCUSD_221230")] #[test_case(MarketType::LinearFuture, "BTCUSDT_221230")] #[test_case(MarketType::InverseSwap, "BTCUSD_PERP")] #[test_case(MarketType::LinearSwap, "BTCUSDT")] // #[test_case(MarketType::EuropeanOption, "BTC-220610-30000-C"; "ignore")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_l2_topk(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_l2_topk, EXCHANGE_NAME, market_type, symbol, MessageType::L2TopK) } #[test_case(MarketType::Spot, "BTCUSDT")] #[test_case(MarketType::InverseFuture, "BTCUSD_221230")] #[test_case(MarketType::LinearFuture, "BTCUSDT_221230")] #[test_case(MarketType::InverseSwap, "BTCUSD_PERP")] #[test_case(MarketType::LinearSwap, "BTCUSDT")] // #[test_case(MarketType::EuropeanOption, "BTC-220610-30000-C")] fn test_crawl_l2_snapshot(market_type: MarketType, symbol: &str) { test_crawl_restful!( crawl_l2_snapshot, EXCHANGE_NAME, market_type, symbol, MessageType::L2Snapshot ) } #[test_case(MarketType::Spot)] #[test_case(MarketType::InverseFuture)] #[test_case(MarketType::LinearFuture)] #[test_case(MarketType::InverseSwap)] #[test_case(MarketType::LinearSwap)] // #[test_case(MarketType::EuropeanOption)] fn test_crawl_l2_snapshot_without_symbol(market_type: MarketType) { test_crawl_restful_all_symbols!( crawl_l2_snapshot, EXCHANGE_NAME, market_type, MessageType::L2Snapshot ) } #[test_case(MarketType::Spot, "BTCUSDT")] #[test_case(MarketType::InverseFuture, "BTCUSD_221230")] #[test_case(MarketType::LinearFuture, "BTCUSDT_221230")] #[test_case(MarketType::InverseSwap, "BTCUSD_PERP")] #[test_case(MarketType::LinearSwap, "BTCUSDT")] // #[test_case(MarketType::EuropeanOption, "BTC-220610-30000-C"; "ignore")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_ticker(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_ticker, EXCHANGE_NAME, market_type, symbol, MessageType::Ticker) } #[test_case(MarketType::InverseSwap, "BTCUSD_PERP")] #[test_case(MarketType::LinearSwap, "BTCUSDT")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_funding_rate(market_type: MarketType, symbol: &str) { test_one_symbol!( crawl_funding_rate, EXCHANGE_NAME, market_type, symbol, MessageType::FundingRate ) } #[test_case(MarketType::Spot)] #[test_case(MarketType::InverseFuture)] #[test_case(MarketType::LinearFuture)] #[test_case(MarketType::InverseSwap)] #[test_case(MarketType::LinearSwap)] #[tokio::test(flavor = "multi_thread", worker_threads = 8)] async fn test_crawl_candlestick(market_type: MarketType) { gen_test_crawl_candlestick!(EXCHANGE_NAME, market_type) } // #[test_case(MarketType::Spot, "BTCUSDT")] // #[test_case(MarketType::InverseFuture, "BTCUSD_221230")] // #[test_case(MarketType::LinearFuture, "BTCUSDT_221230")] // #[test_case(MarketType::InverseSwap, "BTCUSD_PERP")] // #[test_case(MarketType::LinearSwap, "BTCUSDT")] // #[test_case(MarketType::EuropeanOption, "BTC-220610-30000-C"; "ignore")] // #[tokio::test(flavor = "multi_thread")] // async fn test_subscribe_symbol(market_type: MarketType, symbol: &str) { // gen_test_subscribe_symbol!(EXCHANGE_NAME, market_type, symbol) // } ================================================ FILE: crypto-crawler/tests/bitfinex.rs ================================================ #[macro_use] mod utils; use test_case::test_case; use crypto_crawler::*; use crypto_market_type::MarketType; use crypto_msg_type::MessageType; use utils::parse; const EXCHANGE_NAME: &str = "bitfinex"; #[test_case(MarketType::Spot)] #[test_case(MarketType::LinearSwap)] #[tokio::test(flavor = "multi_thread", worker_threads = 8)] async fn test_crawl_trade_all(market_type: MarketType) { test_all_symbols!(crawl_trade, EXCHANGE_NAME, market_type, MessageType::Trade) } #[test_case(MarketType::Spot, "tBTCUSD")] #[test_case(MarketType::LinearSwap, "tBTCF0:USTF0")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_trade(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_trade, EXCHANGE_NAME, market_type, symbol, MessageType::Trade) } #[test_case(MarketType::Spot, "tBTCUSD")] #[test_case(MarketType::LinearSwap, "tBTCF0:USTF0")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_l2_event(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_l2_event, EXCHANGE_NAME, market_type, symbol, MessageType::L2Event) } #[test_case(MarketType::Spot, "tBTCUSD")] #[test_case(MarketType::LinearSwap, "tBTCF0:USTF0")] fn test_crawl_l2_snapshot(market_type: MarketType, symbol: &str) { test_crawl_restful!( crawl_l2_snapshot, EXCHANGE_NAME, market_type, symbol, MessageType::L2Snapshot ) } #[test_case(MarketType::Spot)] #[test_case(MarketType::LinearSwap)] fn test_crawl_l2_snapshot_without_symbol(market_type: MarketType) { test_crawl_restful_all_symbols!( crawl_l2_snapshot, EXCHANGE_NAME, market_type, MessageType::L2Snapshot ) } #[test_case(MarketType::Spot, "tBTCUSD")] #[test_case(MarketType::LinearSwap, "tBTCF0:USTF0")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_l3_event(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_l3_event, EXCHANGE_NAME, market_type, symbol, MessageType::L3Event) } #[test_case(MarketType::Spot, "tBTCUSD")] #[test_case(MarketType::LinearSwap, "tBTCF0:USTF0")] fn test_crawl_l3_snapshot(market_type: MarketType, symbol: &str) { test_crawl_restful!( crawl_l3_snapshot, EXCHANGE_NAME, market_type, symbol, MessageType::L3Snapshot ) } #[test_case(MarketType::Spot, "tBTCUSD")] #[test_case(MarketType::LinearSwap, "tBTCF0:USTF0")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_ticker(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_ticker, EXCHANGE_NAME, market_type, symbol, MessageType::Ticker) } #[test_case(MarketType::Spot)] #[test_case(MarketType::LinearSwap)] #[tokio::test(flavor = "multi_thread", worker_threads = 8)] async fn test_crawl_candlestick(market_type: MarketType) { gen_test_crawl_candlestick!(EXCHANGE_NAME, market_type) } ================================================ FILE: crypto-crawler/tests/bitget.rs ================================================ #[macro_use] mod utils; use test_case::test_case; use crypto_crawler::*; use crypto_market_type::MarketType; use crypto_msg_type::MessageType; use utils::parse; const EXCHANGE_NAME: &str = "bitget"; // #[test_case(MarketType::InverseFuture)] #[test_case(MarketType::InverseSwap)] #[test_case(MarketType::LinearSwap)] #[tokio::test(flavor = "multi_thread", worker_threads = 8)] async fn test_crawl_trade_all(market_type: MarketType) { test_all_symbols!(crawl_trade, EXCHANGE_NAME, market_type, MessageType::Trade) } // #[test_case(MarketType::InverseFuture, "BTCUSD_DMCBL_221230")] #[test_case(MarketType::InverseSwap, "BTCUSD_DMCBL")] #[test_case(MarketType::LinearSwap, "BTCUSDT_UMCBL")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_trade(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_trade, EXCHANGE_NAME, market_type, symbol, MessageType::Trade) } // #[test_case(MarketType::InverseFuture, "BTCUSD_DMCBL_221230")] #[test_case(MarketType::InverseSwap, "BTCUSD_DMCBL")] #[test_case(MarketType::LinearSwap, "BTCUSDT_UMCBL")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_l2_event(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_l2_event, EXCHANGE_NAME, market_type, symbol, MessageType::L2Event) } // #[test_case(MarketType::InverseFuture, "BTCUSD_DMCBL_221230")] #[test_case(MarketType::InverseSwap, "BTCUSD_DMCBL")] #[test_case(MarketType::LinearSwap, "BTCUSDT_UMCBL")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_l2_topk(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_l2_topk, EXCHANGE_NAME, market_type, symbol, MessageType::L2TopK) } #[test_case(MarketType::Spot, "BTCUSDT_SPBL")] #[test_case(MarketType::InverseFuture, "BTCUSD_DMCBL_221230")] #[test_case(MarketType::InverseSwap, "BTCUSD_DMCBL")] #[test_case(MarketType::LinearSwap, "BTCUSDT_UMCBL")] fn test_crawl_l2_snapshot(market_type: MarketType, symbol: &str) { test_crawl_restful!( crawl_l2_snapshot, EXCHANGE_NAME, market_type, symbol, MessageType::L2Snapshot ) } #[test_case(MarketType::Spot)] #[test_case(MarketType::InverseFuture)] #[test_case(MarketType::InverseSwap)] #[test_case(MarketType::LinearSwap)] fn test_crawl_l2_snapshot_without_symbol(market_type: MarketType) { test_crawl_restful_all_symbols!( crawl_l2_snapshot, EXCHANGE_NAME, market_type, MessageType::L2Snapshot ) } // #[test_case(MarketType::InverseFuture, "BTCUSD_DMCBL_221230")] #[test_case(MarketType::InverseSwap, "BTCUSD_DMCBL")] #[test_case(MarketType::LinearSwap, "BTCUSDT_UMCBL")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_ticker(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_ticker, EXCHANGE_NAME, market_type, symbol, MessageType::Ticker) } // #[test_case(MarketType::InverseFuture)] #[test_case(MarketType::InverseSwap)] #[test_case(MarketType::LinearSwap)] #[tokio::test(flavor = "multi_thread", worker_threads = 16)] async fn test_crawl_candlestick(market_type: MarketType) { gen_test_crawl_candlestick!(EXCHANGE_NAME, market_type) } ================================================ FILE: crypto-crawler/tests/bithumb.rs ================================================ #[macro_use] mod utils; use test_case::test_case; use crypto_crawler::*; use crypto_market_type::MarketType; use crypto_msg_type::MessageType; use utils::parse; const EXCHANGE_NAME: &str = "bithumb"; #[test_case(MarketType::Spot)] #[tokio::test(flavor = "multi_thread", worker_threads = 8)] async fn test_crawl_trade_all(market_type: MarketType) { test_all_symbols!(crawl_trade, EXCHANGE_NAME, market_type, MessageType::Trade) } #[test_case(MarketType::Spot, "BTC-USDT")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_trade(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_trade, EXCHANGE_NAME, market_type, symbol, MessageType::Trade) } #[test_case(MarketType::Spot, "BTC-USDT")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_l2_event(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_l2_event, EXCHANGE_NAME, market_type, symbol, MessageType::L2Event) } #[test_case(MarketType::Spot, "BTC-USDT")] fn test_crawl_l2_snapshot(market_type: MarketType, symbol: &str) { test_crawl_restful!( crawl_l2_snapshot, EXCHANGE_NAME, market_type, symbol, MessageType::L2Snapshot ) } #[test_case(MarketType::Spot)] fn test_crawl_l2_snapshot_without_symbol(market_type: MarketType) { test_crawl_restful_all_symbols!( crawl_l2_snapshot, EXCHANGE_NAME, market_type, MessageType::L2Snapshot ) } #[test_case(MarketType::Spot, "BTC-USDT")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_ticker(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_ticker, EXCHANGE_NAME, market_type, symbol, MessageType::Ticker) } ================================================ FILE: crypto-crawler/tests/bitmex.rs ================================================ #[macro_use] mod utils; use test_case::test_case; use crypto_crawler::*; use crypto_market_type::MarketType; use crypto_msg_type::MessageType; use utils::parse; const EXCHANGE_NAME: &str = "bitmex"; async fn crawl_all(msg_type: MessageType) { let (tx, rx) = std::sync::mpsc::channel(); tokio::task::spawn(async move { match msg_type { MessageType::Trade => { crawl_trade(EXCHANGE_NAME, MarketType::Unknown, None, tx).await; } MessageType::L2Event => { crawl_l2_event(EXCHANGE_NAME, MarketType::Unknown, None, tx).await; } MessageType::L2Snapshot => { tokio::task::block_in_place(move || { crawl_l2_snapshot(EXCHANGE_NAME, MarketType::Unknown, None, tx); }); } MessageType::BBO => { crawl_bbo(EXCHANGE_NAME, MarketType::Unknown, None, tx).await; } MessageType::L2TopK => { crawl_l2_topk(EXCHANGE_NAME, MarketType::Unknown, None, tx).await; } MessageType::FundingRate => { crawl_funding_rate(EXCHANGE_NAME, MarketType::Unknown, None, tx).await; } _ => panic!("unsupported message type {msg_type}"), }; }); let msg = rx.recv().unwrap(); assert_eq!(msg.exchange, EXCHANGE_NAME.to_string()); assert_eq!(msg.market_type, MarketType::Unknown); assert_eq!(msg.msg_type, msg_type); } #[tokio::test(flavor = "multi_thread")] async fn test_crawl_trade_all() { crawl_all(MessageType::Trade).await; } #[tokio::test(flavor = "multi_thread")] async fn test_crawl_l2_event_all() { crawl_all(MessageType::L2Event).await; } #[tokio::test(flavor = "multi_thread")] async fn test_crawl_bbo_all() { crawl_all(MessageType::BBO).await; } #[tokio::test(flavor = "multi_thread")] async fn test_crawl_l2_topk_all() { crawl_all(MessageType::L2TopK).await; } #[tokio::test(flavor = "multi_thread")] async fn test_crawl_l2_snapshot_all() { crawl_all(MessageType::L2Snapshot).await; } #[tokio::test(flavor = "multi_thread")] async fn test_crawl_funding_rate_all() { crawl_all(MessageType::FundingRate).await; } #[tokio::test(flavor = "multi_thread")] async fn test_crawl_candlestick_rate_all() { let (tx, rx) = std::sync::mpsc::channel(); tokio::task::spawn(async move { crawl_candlestick(EXCHANGE_NAME, MarketType::Unknown, None, tx).await; }); let msg = rx.recv().unwrap(); assert_eq!(msg.exchange, EXCHANGE_NAME.to_string()); assert_eq!(msg.market_type, MarketType::Unknown); assert_eq!(msg.msg_type, MessageType::Candlestick); } #[test_case(MarketType::InverseSwap, "XBTUSD")] #[test_case(MarketType::QuantoSwap, "ETHUSD")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_l2_event(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_l2_event, EXCHANGE_NAME, market_type, symbol, MessageType::L2Event) } // #[test_case(MarketType::InverseSwap, "XBTUSD")] // fn test_subscribe_symbol(market_type: MarketType, symbol: &str) { // gen_test_subscribe_symbol!(EXCHANGE_NAME, market_type, symbol) // } ================================================ FILE: crypto-crawler/tests/bitstamp.rs ================================================ #[macro_use] mod utils; use test_case::test_case; use crypto_crawler::*; use crypto_market_type::MarketType; use crypto_msg_type::MessageType; use utils::parse; const EXCHANGE_NAME: &str = "bitstamp"; #[test_case(MarketType::Spot)] #[tokio::test(flavor = "multi_thread", worker_threads = 8)] async fn test_crawl_trade_all(market_type: MarketType) { test_all_symbols!(crawl_trade, EXCHANGE_NAME, market_type, MessageType::Trade) } #[test_case(MarketType::Spot, "btcusd")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_trade(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_trade, EXCHANGE_NAME, market_type, symbol, MessageType::Trade) } #[test_case(MarketType::Spot, "btcusd")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_l2_event(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_l2_event, EXCHANGE_NAME, market_type, symbol, MessageType::L2Event) } #[test_case(MarketType::Spot, "btcusd")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_l2_topk(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_l2_topk, EXCHANGE_NAME, market_type, symbol, MessageType::L2TopK) } #[test_case(MarketType::Spot, "btcusd")] fn test_crawl_l2_snapshot(market_type: MarketType, symbol: &str) { test_crawl_restful!( crawl_l2_snapshot, EXCHANGE_NAME, market_type, symbol, MessageType::L2Snapshot ) } #[test_case(MarketType::Spot)] fn test_crawl_l2_snapshot_without_symbol(market_type: MarketType) { test_crawl_restful_all_symbols!( crawl_l2_snapshot, EXCHANGE_NAME, market_type, MessageType::L2Snapshot ) } #[test_case(MarketType::Spot, "btcusd")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_l3_event(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_l3_event, EXCHANGE_NAME, market_type, symbol, MessageType::L3Event) } #[test_case(MarketType::Spot, "btcusd")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_l3_snapshot(market_type: MarketType, symbol: &str) { test_crawl_restful!( crawl_l3_snapshot, EXCHANGE_NAME, market_type, symbol, MessageType::L3Snapshot ) } ================================================ FILE: crypto-crawler/tests/bitz.rs ================================================ #[macro_use] mod utils; use test_case::test_case; use crypto_crawler::*; use crypto_market_type::MarketType; use crypto_msg_type::MessageType; use utils::parse; const EXCHANGE_NAME: &str = "bitz"; #[test_case(MarketType::Spot, "btc_usdt"; "inconclusive")] // #[test_case(MarketType::InverseSwap, "BTC_USD")] // #[test_case(MarketType::LinearSwap, "BTC_USDT")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_trade(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_trade, EXCHANGE_NAME, market_type, symbol, MessageType::Trade) } #[test_case(MarketType::Spot, "btc_usdt"; "inconclusive")] // #[test_case(MarketType::InverseSwap, "BTC_USD")] // #[test_case(MarketType::LinearSwap, "BTC_USDT")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_l2_event(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_l2_event, EXCHANGE_NAME, market_type, symbol, MessageType::L2Event) } #[test_case(MarketType::Spot, "btc_usdt"; "inconclusive spot")] #[test_case(MarketType::InverseSwap, "BTC_USD"; "inconclusive inverse_swap")] #[test_case(MarketType::LinearSwap, "BTC_USDT"; "inconclusive linear_swap")] fn test_crawl_l2_snapshot(market_type: MarketType, symbol: &str) { test_crawl_restful!( crawl_l2_snapshot, EXCHANGE_NAME, market_type, symbol, MessageType::L2Snapshot ) } #[test_case(MarketType::Spot; "inconclusive spot")] #[test_case(MarketType::InverseSwap; "inconclusive inverse_swap")] #[test_case(MarketType::LinearSwap; "inconclusive linear_swap")] fn test_crawl_l2_snapshot_without_symbol(market_type: MarketType) { test_crawl_restful_all_symbols!( crawl_l2_snapshot, EXCHANGE_NAME, market_type, MessageType::L2Snapshot ) } #[test_case(MarketType::Spot, "btc_usdt"; "inconclusive")] // #[test_case(MarketType::InverseSwap, "BTC_USD")] // #[test_case(MarketType::LinearSwap, "BTC_USDT")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_ticker(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_ticker, EXCHANGE_NAME, market_type, symbol, MessageType::Ticker) } #[test_case(MarketType::Spot; "inconclusive")] #[tokio::test(flavor = "multi_thread", worker_threads = 8)] async fn test_crawl_candlestick(market_type: MarketType) { gen_test_crawl_candlestick!(EXCHANGE_NAME, market_type) } ================================================ FILE: crypto-crawler/tests/bybit.rs ================================================ #[macro_use] mod utils; use test_case::test_case; use crypto_crawler::*; use crypto_market_type::MarketType; use crypto_msg_type::MessageType; use utils::parse; const EXCHANGE_NAME: &str = "bybit"; #[test_case(MarketType::InverseFuture)] #[test_case(MarketType::InverseSwap)] #[test_case(MarketType::LinearSwap)] #[tokio::test(flavor = "multi_thread", worker_threads = 8)] async fn test_crawl_trade_all(market_type: MarketType) { test_all_symbols!(crawl_trade, EXCHANGE_NAME, market_type, MessageType::Trade) } #[test_case(MarketType::InverseFuture, "BTCUSDZ22")] #[test_case(MarketType::InverseSwap, "BTCUSD")] #[test_case(MarketType::LinearSwap, "BTCUSDT")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_trade(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_trade, EXCHANGE_NAME, market_type, symbol, MessageType::Trade) } #[test_case(MarketType::InverseFuture, "BTCUSDZ22")] #[test_case(MarketType::InverseSwap, "BTCUSD")] #[test_case(MarketType::LinearSwap, "BTCUSDT")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_l2_event(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_l2_event, EXCHANGE_NAME, market_type, symbol, MessageType::L2Event) } #[test_case(MarketType::InverseFuture, "BTCUSDZ22")] #[test_case(MarketType::InverseSwap, "BTCUSD")] #[test_case(MarketType::LinearSwap, "BTCUSDT")] fn test_crawl_l2_snapshot(market_type: MarketType, symbol: &str) { test_crawl_restful!( crawl_l2_snapshot, EXCHANGE_NAME, market_type, symbol, MessageType::L2Snapshot ) } #[test_case(MarketType::InverseFuture)] #[test_case(MarketType::InverseSwap)] #[test_case(MarketType::LinearSwap)] fn test_crawl_l2_snapshot_without_symbol(market_type: MarketType) { test_crawl_restful_all_symbols!( crawl_l2_snapshot, EXCHANGE_NAME, market_type, MessageType::L2Snapshot ) } #[test_case(MarketType::InverseFuture, "BTCUSDZ22")] #[test_case(MarketType::InverseSwap, "BTCUSD")] #[test_case(MarketType::LinearSwap, "BTCUSDT")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_ticker(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_ticker, EXCHANGE_NAME, market_type, symbol, MessageType::Ticker) } #[test_case(MarketType::InverseFuture)] #[test_case(MarketType::InverseSwap)] #[test_case(MarketType::LinearSwap)] #[tokio::test(flavor = "multi_thread", worker_threads = 8)] async fn test_crawl_candlestick(market_type: MarketType) { gen_test_crawl_candlestick!(EXCHANGE_NAME, market_type) } // #[test_case(MarketType::InverseFuture, "BTCUSDZ22")] // #[test_case(MarketType::InverseSwap, "BTCUSD")] // #[test_case(MarketType::LinearSwap, "BTCUSDT")] // fn test_subscribe_symbol(market_type: MarketType, symbol: &str) { // gen_test_subscribe_symbol!(EXCHANGE_NAME, market_type, symbol) // } ================================================ FILE: crypto-crawler/tests/coinbase_pro.rs ================================================ #[macro_use] mod utils; use test_case::test_case; use crypto_crawler::*; use crypto_market_type::MarketType; use crypto_msg_type::MessageType; use utils::parse; const EXCHANGE_NAME: &str = "coinbase_pro"; #[test_case(MarketType::Spot)] #[tokio::test(flavor = "multi_thread", worker_threads = 8)] async fn test_crawl_trade_all(market_type: MarketType) { test_all_symbols!(crawl_trade, EXCHANGE_NAME, market_type, MessageType::Trade) } #[test_case(MarketType::Spot, "BTC-USD")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_trade(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_trade, EXCHANGE_NAME, market_type, symbol, MessageType::Trade) } #[test_case(MarketType::Spot, "BTC-USD")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_l2_event(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_l2_event, EXCHANGE_NAME, market_type, symbol, MessageType::L2Event) } #[test_case(MarketType::Spot, "BTC-USD")] fn test_crawl_l2_snapshot(market_type: MarketType, symbol: &str) { test_crawl_restful!( crawl_l2_snapshot, EXCHANGE_NAME, market_type, symbol, MessageType::L2Snapshot ) } #[test_case(MarketType::Spot)] fn test_crawl_l2_snapshot_without_symbol(market_type: MarketType) { test_crawl_restful_all_symbols!( crawl_l2_snapshot, EXCHANGE_NAME, market_type, MessageType::L2Snapshot ) } #[test_case(MarketType::Spot, "BTC-USD")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_l3_event(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_l3_event, EXCHANGE_NAME, market_type, symbol, MessageType::L3Event) } #[test_case(MarketType::Spot, "BTC-USD")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_l3_snapshot(market_type: MarketType, symbol: &str) { test_crawl_restful!( crawl_l3_snapshot, EXCHANGE_NAME, market_type, symbol, MessageType::L3Snapshot ) } #[test_case(MarketType::Spot, "BTC-USD")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_ticker(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_ticker, EXCHANGE_NAME, market_type, symbol, MessageType::Ticker) } ================================================ FILE: crypto-crawler/tests/deribit.rs ================================================ #[macro_use] mod utils; use test_case::test_case; use crypto_crawler::*; use crypto_market_type::MarketType; use crypto_msg_type::MessageType; use utils::parse; const EXCHANGE_NAME: &str = "deribit"; #[test_case(MarketType::InverseSwap)] #[test_case(MarketType::InverseFuture)] #[test_case(MarketType::EuropeanOption)] #[tokio::test(flavor = "multi_thread", worker_threads = 8)] async fn test_crawl_trade_all(market_type: MarketType) { test_all_symbols!(crawl_trade, EXCHANGE_NAME, market_type, MessageType::Trade) } #[test_case(MarketType::InverseSwap, "BTC-PERPETUAL")] // #[test_case(MarketType::InverseFuture, "BTC-30DEC22")] // #[test_case(MarketType::EuropeanOption, "BTC-30DEC22-25000-C")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_trade(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_trade, EXCHANGE_NAME, market_type, symbol, MessageType::Trade) } #[test_case(MarketType::InverseSwap, "BTC-PERPETUAL")] #[test_case(MarketType::InverseFuture, "BTC-30DEC22")] #[test_case(MarketType::EuropeanOption, "BTC-30DEC22-25000-C")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_l2_event(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_l2_event, EXCHANGE_NAME, market_type, symbol, MessageType::L2Event) } #[test_case(MarketType::InverseSwap, "BTC-PERPETUAL")] #[test_case(MarketType::InverseFuture, "BTC-30DEC22")] #[test_case(MarketType::EuropeanOption, "BTC-30DEC22-25000-C")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_bbo(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_bbo, EXCHANGE_NAME, market_type, symbol, MessageType::BBO) } #[test_case(MarketType::InverseSwap, "BTC-PERPETUAL")] #[test_case(MarketType::InverseFuture, "BTC-30DEC22")] #[test_case(MarketType::EuropeanOption, "BTC-30DEC22-25000-C")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_l2_topk(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_l2_topk, EXCHANGE_NAME, market_type, symbol, MessageType::L2TopK) } #[test_case(MarketType::InverseSwap, "BTC-PERPETUAL")] #[test_case(MarketType::InverseFuture, "BTC-30DEC22")] #[test_case(MarketType::EuropeanOption, "BTC-30DEC22-25000-C")] fn test_crawl_l2_snapshot(market_type: MarketType, symbol: &str) { test_crawl_restful!( crawl_l2_snapshot, EXCHANGE_NAME, market_type, symbol, MessageType::L2Snapshot ) } #[test_case(MarketType::InverseSwap)] #[test_case(MarketType::InverseFuture)] #[test_case(MarketType::EuropeanOption; "inconclusive")] fn test_crawl_l2_snapshot_without_symbol(market_type: MarketType) { test_crawl_restful_all_symbols!( crawl_l2_snapshot, EXCHANGE_NAME, market_type, MessageType::L2Snapshot ) } #[test_case(MarketType::InverseSwap, "BTC-PERPETUAL")] #[test_case(MarketType::InverseFuture, "BTC-30DEC22")] #[test_case(MarketType::EuropeanOption, "BTC-30DEC22-25000-C")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_ticker(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_ticker, EXCHANGE_NAME, market_type, symbol, MessageType::Ticker) } #[test_case(MarketType::InverseSwap)] #[test_case(MarketType::InverseFuture)] #[test_case(MarketType::EuropeanOption)] #[tokio::test(flavor = "multi_thread", worker_threads = 8)] async fn test_crawl_candlestick(market_type: MarketType) { gen_test_crawl_candlestick!(EXCHANGE_NAME, market_type) } // #[test_case(MarketType::InverseSwap, "BTC-PERPETUAL")] // #[test_case(MarketType::InverseFuture, "BTC-30DEC22")] // #[test_case(MarketType::EuropeanOption, "BTC-30DEC22-25000-C")] // fn test_subscribe_symbol(market_type: MarketType, symbol: &str) { // gen_test_subscribe_symbol!(EXCHANGE_NAME, market_type, symbol) // } ================================================ FILE: crypto-crawler/tests/dydx.rs ================================================ #[macro_use] mod utils; use test_case::test_case; use crypto_crawler::*; use crypto_market_type::MarketType; use crypto_msg_type::MessageType; use utils::parse; const EXCHANGE_NAME: &str = "dydx"; #[test_case(MarketType::LinearSwap)] #[tokio::test(flavor = "multi_thread", worker_threads = 8)] async fn test_crawl_trade_all(market_type: MarketType) { test_all_symbols!(crawl_trade, EXCHANGE_NAME, market_type, MessageType::Trade) } #[test_case(MarketType::LinearSwap, "BTC-USD")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_trade(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_trade, EXCHANGE_NAME, market_type, symbol, MessageType::Trade) } #[test_case(MarketType::LinearSwap, "BTC-USD")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_l2_event(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_l2_event, EXCHANGE_NAME, market_type, symbol, MessageType::L2Event) } #[test_case(MarketType::LinearSwap)] fn test_crawl_l2_snapshot_without_symbol(market_type: MarketType) { test_crawl_restful_all_symbols!( crawl_l2_snapshot, EXCHANGE_NAME, market_type, MessageType::L2Snapshot ) } ================================================ FILE: crypto-crawler/tests/ftx.rs ================================================ #[macro_use] mod utils; use test_case::test_case; use crypto_crawler::*; use crypto_market_type::MarketType; use crypto_msg_type::MessageType; use utils::parse; const EXCHANGE_NAME: &str = "ftx"; #[test_case(MarketType::Spot)] #[test_case(MarketType::LinearSwap)] #[test_case(MarketType::LinearFuture)] // #[test_case(MarketType::Move)] // #[test_case(MarketType::BVOL)] #[tokio::test(flavor = "multi_thread", worker_threads = 8)] async fn test_crawl_trade_all(market_type: MarketType) { test_all_symbols!(crawl_trade, EXCHANGE_NAME, market_type, MessageType::Trade) } #[test_case(MarketType::Spot, "BTC/USD")] #[test_case(MarketType::LinearSwap, "BTC-PERP")] #[test_case(MarketType::LinearFuture, "BTC-1230")] // #[test_case(MarketType::Move, "BTC-MOVE-2022Q4")] // #[test_case(MarketType::BVOL, "BVOL/USD")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_trade(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_trade, EXCHANGE_NAME, market_type, symbol, MessageType::Trade) } #[test_case(MarketType::Spot, "BTC/USD")] #[test_case(MarketType::LinearSwap, "BTC-PERP")] #[test_case(MarketType::LinearFuture, "BTC-1230")] #[test_case(MarketType::Move, "BTC-MOVE-2022Q4")] #[test_case(MarketType::BVOL, "BVOL/USD")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_l2_event(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_l2_event, EXCHANGE_NAME, market_type, symbol, MessageType::L2Event) } #[test_case(MarketType::Spot, "BTC/USD")] #[test_case(MarketType::LinearSwap, "BTC-PERP")] #[test_case(MarketType::LinearFuture, "BTC-1230")] #[test_case(MarketType::Move, "BTC-MOVE-2022Q4")] #[test_case(MarketType::BVOL, "BVOL/USD")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_bbo(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_bbo, EXCHANGE_NAME, market_type, symbol, MessageType::BBO) } #[test_case(MarketType::Spot, "BTC/USD")] #[test_case(MarketType::LinearSwap, "BTC-PERP")] #[test_case(MarketType::LinearFuture, "BTC-1230")] #[test_case(MarketType::Move, "BTC-MOVE-2022Q4")] #[test_case(MarketType::BVOL, "BVOL/USD")] fn test_crawl_l2_snapshot(market_type: MarketType, symbol: &str) { test_crawl_restful!( crawl_l2_snapshot, EXCHANGE_NAME, market_type, symbol, MessageType::L2Snapshot ) } #[test_case(MarketType::Spot)] #[test_case(MarketType::LinearSwap)] #[test_case(MarketType::LinearFuture)] #[test_case(MarketType::Move)] #[test_case(MarketType::BVOL)] fn test_crawl_l2_snapshot_without_symbol(market_type: MarketType) { test_crawl_restful_all_symbols!( crawl_l2_snapshot, EXCHANGE_NAME, market_type, MessageType::L2Snapshot ) } // #[test_case(MarketType::Spot, "BTC/USD")] // #[test_case(MarketType::LinearSwap, "BTC-PERP")] // #[test_case(MarketType::LinearFuture, "BTC-1230")] // fn test_subscribe_symbol(market_type: MarketType, symbol: &str) { // gen_test_subscribe_symbol!(EXCHANGE_NAME, market_type, symbol) // } ================================================ FILE: crypto-crawler/tests/gate.rs ================================================ #[macro_use] mod utils; use test_case::test_case; use crypto_crawler::*; use crypto_market_type::MarketType; use crypto_msg_type::MessageType; use utils::parse; const EXCHANGE_NAME: &str = "gate"; #[test_case(MarketType::Spot)] #[test_case(MarketType::InverseSwap)] #[test_case(MarketType::LinearSwap)] // #[test_case(MarketType::InverseFuture)] // #[test_case(MarketType::LinearFuture)] #[tokio::test(flavor = "multi_thread", worker_threads = 8)] async fn test_crawl_trade_all(market_type: MarketType) { test_all_symbols!(crawl_trade, EXCHANGE_NAME, market_type, MessageType::Trade) } #[test_case(MarketType::Spot, "BTC_USDT")] // #[test_case(MarketType::InverseSwap, "BTC_USD")] #[test_case(MarketType::LinearSwap, "BTC_USDT")] // #[test_case(MarketType::InverseFuture, "BTC_USD_20221230"; "ignore")] // #[test_case(MarketType::LinearFuture, "BTC_USDT_20221230"; "ignore")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_trade(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_trade, EXCHANGE_NAME, market_type, symbol, MessageType::Trade) } #[test_case(MarketType::Spot, "BTC_USDT")] #[test_case(MarketType::InverseSwap, "BTC_USD")] #[test_case(MarketType::LinearSwap, "BTC_USDT")] #[test_case(MarketType::InverseFuture, "BTC_USD_20221230")] #[test_case(MarketType::LinearFuture, "BTC_USDT_20221230")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_l2_event(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_l2_event, EXCHANGE_NAME, market_type, symbol, MessageType::L2Event) } #[test_case(MarketType::Spot, "BTC_USDT")] #[test_case(MarketType::InverseSwap, "BTC_USD")] #[test_case(MarketType::LinearSwap, "BTC_USDT")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_bbo(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_bbo, EXCHANGE_NAME, market_type, symbol, MessageType::BBO) } #[test_case(MarketType::Spot, "BTC_USDT")] #[test_case(MarketType::InverseSwap, "BTC_USD")] #[test_case(MarketType::LinearSwap, "BTC_USDT")] #[test_case(MarketType::InverseFuture, "BTC_USD_20221230")] #[test_case(MarketType::LinearFuture, "BTC_USDT_20221230")] fn test_crawl_l2_snapshot(market_type: MarketType, symbol: &str) { test_crawl_restful!( crawl_l2_snapshot, EXCHANGE_NAME, market_type, symbol, MessageType::L2Snapshot ) } #[test_case(MarketType::Spot)] #[test_case(MarketType::InverseSwap)] #[test_case(MarketType::LinearSwap)] #[test_case(MarketType::InverseFuture)] #[test_case(MarketType::LinearFuture)] fn test_crawl_l2_snapshot_without_symbol(market_type: MarketType) { test_crawl_restful_all_symbols!( crawl_l2_snapshot, EXCHANGE_NAME, market_type, MessageType::L2Snapshot ) } #[test_case(MarketType::Spot, "BTC_USDT")] #[test_case(MarketType::InverseSwap, "BTC_USD")] #[test_case(MarketType::LinearSwap, "BTC_USDT")] // #[test_case(MarketType::InverseFuture, "BTC_USD_20221230")] #[test_case(MarketType::LinearFuture, "BTC_USDT_20221230")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_ticker(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_ticker, EXCHANGE_NAME, market_type, symbol, MessageType::Ticker) } // #[test_case(MarketType::Spot; "inconclusive due to too many symbols")] #[test_case(MarketType::InverseSwap)] // #[test_case(MarketType::LinearSwap)] // always timeout in Github workflow // #[test_case(MarketType::InverseFuture)] // #[test_case(MarketType::LinearFuture)] #[tokio::test(flavor = "multi_thread", worker_threads = 8)] async fn test_crawl_candlestick(market_type: MarketType) { gen_test_crawl_candlestick!(EXCHANGE_NAME, market_type) } ================================================ FILE: crypto-crawler/tests/huobi.rs ================================================ #[macro_use] mod utils; use test_case::test_case; use crypto_crawler::*; use crypto_market_type::MarketType; use crypto_msg_type::MessageType; use utils::parse; const EXCHANGE_NAME: &str = "huobi"; #[test_case(MarketType::Spot)] #[test_case(MarketType::InverseFuture)] #[test_case(MarketType::InverseSwap)] #[test_case(MarketType::LinearSwap)] #[tokio::test(flavor = "multi_thread", worker_threads = 8)] async fn test_crawl_trade_all(market_type: MarketType) { test_all_symbols!(crawl_trade, EXCHANGE_NAME, market_type, MessageType::Trade) } #[test_case(MarketType::Spot, "btcusdt")] #[test_case(MarketType::InverseFuture, "BTC_CQ")] #[test_case(MarketType::InverseSwap, "BTC-USD")] #[test_case(MarketType::LinearSwap, "BTC-USDT")] #[test_case(MarketType::EuropeanOption, "BTC-USDT-210625-P-27000"; "inconclusive")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_trade(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_trade, EXCHANGE_NAME, market_type, symbol, MessageType::Trade) } #[test_case(MarketType::Spot, "btcusdt")] #[test_case(MarketType::InverseFuture, "BTC_CQ")] #[test_case(MarketType::InverseSwap, "BTC-USD")] #[test_case(MarketType::LinearSwap, "BTC-USDT")] #[test_case(MarketType::EuropeanOption, "BTC-USDT-210625-P-27000"; "inconclusive")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_l2_event(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_l2_event, EXCHANGE_NAME, market_type, symbol, MessageType::L2Event) } #[test_case(MarketType::Spot, "btcusdt")] #[test_case(MarketType::InverseFuture, "BTC_CQ")] #[test_case(MarketType::InverseSwap, "BTC-USD")] #[test_case(MarketType::LinearSwap, "BTC-USDT")] #[test_case(MarketType::EuropeanOption, "BTC-USDT-210625-P-27000"; "inconclusive")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_bbo(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_bbo, EXCHANGE_NAME, market_type, symbol, MessageType::BBO) } #[test_case(MarketType::Spot, "btcusdt")] #[test_case(MarketType::InverseFuture, "BTC_CQ")] #[test_case(MarketType::InverseSwap, "BTC-USD")] #[test_case(MarketType::LinearSwap, "BTC-USDT")] #[test_case(MarketType::EuropeanOption, "BTC-USDT-210625-P-27000"; "inconclusive")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_l2_topk(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_l2_topk, EXCHANGE_NAME, market_type, symbol, MessageType::L2TopK) } #[test_case(MarketType::Spot, "btcusdt")] #[test_case(MarketType::InverseFuture, "BTC_CQ")] #[test_case(MarketType::InverseSwap, "BTC-USD")] #[test_case(MarketType::LinearSwap, "BTC-USDT")] #[test_case(MarketType::EuropeanOption, "BTC-USDT-210625-P-27000"; "inconclusive")] fn test_crawl_l2_snapshot(market_type: MarketType, symbol: &str) { test_crawl_restful!( crawl_l2_snapshot, EXCHANGE_NAME, market_type, symbol, MessageType::L2Snapshot ) } #[test_case(MarketType::Spot)] #[test_case(MarketType::InverseFuture)] #[test_case(MarketType::InverseSwap)] #[test_case(MarketType::LinearSwap)] #[test_case(MarketType::EuropeanOption; "inconclusive")] fn test_crawl_l2_snapshot_without_symbol(market_type: MarketType) { test_crawl_restful_all_symbols!( crawl_l2_snapshot, EXCHANGE_NAME, market_type, MessageType::L2Snapshot ) } #[test_case(MarketType::InverseSwap, "BTC-USD")] #[test_case(MarketType::LinearSwap, "BTC-USDT")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_funding_rate(market_type: MarketType, symbol: &str) { test_one_symbol!( crawl_funding_rate, EXCHANGE_NAME, market_type, symbol, MessageType::FundingRate ) } #[test_case(MarketType::Spot, "btcusdt")] #[test_case(MarketType::InverseFuture, "BTC_CQ")] #[test_case(MarketType::InverseSwap, "BTC-USD")] #[test_case(MarketType::LinearSwap, "BTC-USDT")] #[test_case(MarketType::EuropeanOption, "BTC-USDT-210625-P-27000"; "inconclusive")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_ticker(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_ticker, EXCHANGE_NAME, market_type, symbol, MessageType::Ticker) } #[test_case(MarketType::Spot)] #[test_case(MarketType::InverseFuture)] #[test_case(MarketType::InverseSwap)] #[test_case(MarketType::LinearSwap)] #[test_case(MarketType::EuropeanOption; "inconclusive")] #[tokio::test(flavor = "multi_thread", worker_threads = 8)] async fn test_crawl_candlestick(market_type: MarketType) { gen_test_crawl_candlestick!(EXCHANGE_NAME, market_type) } // #[test_case(MarketType::Spot, "btcusdt")] // #[test_case(MarketType::InverseFuture, "BTC_CQ")] // #[test_case(MarketType::InverseSwap, "BTC-USD")] // #[test_case(MarketType::LinearSwap, "BTC-USDT")] // fn test_subscribe_symbol(market_type: MarketType, symbol: &str) { // gen_test_subscribe_symbol!(EXCHANGE_NAME, market_type, symbol) // } ================================================ FILE: crypto-crawler/tests/kraken.rs ================================================ #[macro_use] mod utils; use test_case::test_case; use crypto_crawler::*; use crypto_market_type::MarketType; use crypto_msg_type::MessageType; use utils::parse; const EXCHANGE_NAME: &str = "kraken"; #[test_case(MarketType::Spot)] #[test_case(MarketType::InverseFuture)] #[test_case(MarketType::InverseSwap)] #[tokio::test(flavor = "multi_thread", worker_threads = 8)] async fn test_crawl_trade_all(market_type: MarketType) { test_all_symbols!(crawl_trade, EXCHANGE_NAME, market_type, MessageType::Trade) } #[test_case(MarketType::Spot, "XBT/USD")] #[test_case(MarketType::InverseFuture, "FI_XBTUSD_221230")] #[test_case(MarketType::InverseSwap, "PI_XBTUSD")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_trade(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_trade, EXCHANGE_NAME, market_type, symbol, MessageType::Trade) } #[test_case(MarketType::Spot, "XBT/USD")] #[test_case(MarketType::InverseFuture, "FI_XBTUSD_221230")] #[test_case(MarketType::InverseSwap, "PI_XBTUSD")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_l2_event(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_l2_event, EXCHANGE_NAME, market_type, symbol, MessageType::L2Event) } #[test_case(MarketType::Spot, "XBT/USD")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_bbo(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_bbo, EXCHANGE_NAME, market_type, symbol, MessageType::BBO) } #[test_case(MarketType::Spot, "XBT/USD")] #[test_case(MarketType::InverseFuture, "FI_XBTUSD_221230")] #[test_case(MarketType::InverseSwap, "PI_XBTUSD")] fn test_crawl_l2_snapshot(market_type: MarketType, symbol: &str) { test_crawl_restful!( crawl_l2_snapshot, EXCHANGE_NAME, market_type, symbol, MessageType::L2Snapshot ) } #[test_case(MarketType::Spot)] #[test_case(MarketType::InverseFuture)] #[test_case(MarketType::InverseSwap)] fn test_crawl_l2_snapshot_without_symbol(market_type: MarketType) { test_crawl_restful_all_symbols!( crawl_l2_snapshot, EXCHANGE_NAME, market_type, MessageType::L2Snapshot ) } #[test_case(MarketType::Spot, "XBT/USD")] #[test_case(MarketType::InverseFuture, "FI_XBTUSD_221230")] #[test_case(MarketType::InverseSwap, "PI_XBTUSD")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_ticker(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_ticker, EXCHANGE_NAME, market_type, symbol, MessageType::Ticker) } #[test_case(MarketType::Spot)] #[tokio::test(flavor = "multi_thread", worker_threads = 8)] async fn test_crawl_candlestick(market_type: MarketType) { gen_test_crawl_candlestick!(EXCHANGE_NAME, market_type) } ================================================ FILE: crypto-crawler/tests/kucoin.rs ================================================ #[macro_use] mod utils; use test_case::test_case; use crypto_crawler::*; use crypto_market_type::MarketType; use crypto_msg_type::MessageType; use utils::parse; const EXCHANGE_NAME: &str = "kucoin"; #[test_case(MarketType::Spot)] #[test_case(MarketType::InverseSwap)] #[test_case(MarketType::LinearSwap)] #[tokio::test(flavor = "multi_thread", worker_threads = 8)] async fn test_crawl_trade_all(market_type: MarketType) { test_all_symbols!(crawl_trade, EXCHANGE_NAME, market_type, MessageType::Trade) } #[test_case(MarketType::Spot, "BTC-USDT")] // #[test_case(MarketType::InverseSwap, "XBTUSDM")] #[test_case(MarketType::LinearSwap, "XBTUSDTM")] // #[test_case(MarketType::InverseFuture, "XBTMZ22"; "ignore")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_trade(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_trade, EXCHANGE_NAME, market_type, symbol, MessageType::Trade) } #[test_case(MarketType::Spot, "BTC-USDT")] #[test_case(MarketType::InverseSwap, "XBTUSDM")] #[test_case(MarketType::LinearSwap, "XBTUSDTM")] #[test_case(MarketType::InverseFuture, "XBTMZ22")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_l2_event(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_l2_event, EXCHANGE_NAME, market_type, symbol, MessageType::L2Event) } #[test_case(MarketType::Spot, "BTC-USDT")] #[test_case(MarketType::InverseSwap, "XBTUSDM")] #[test_case(MarketType::LinearSwap, "XBTUSDTM")] #[test_case(MarketType::InverseFuture, "XBTMZ22")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_bbo(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_bbo, EXCHANGE_NAME, market_type, symbol, MessageType::BBO) } #[test_case(MarketType::Spot, "BTC-USDT")] #[test_case(MarketType::InverseSwap, "XBTUSDM")] #[test_case(MarketType::LinearSwap, "XBTUSDTM")] #[test_case(MarketType::InverseFuture, "XBTMZ22")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_l2_topk(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_l2_topk, EXCHANGE_NAME, market_type, symbol, MessageType::L2TopK) } #[test_case(MarketType::Spot, "BTC-USDT")] #[test_case(MarketType::InverseSwap, "XBTUSDM")] #[test_case(MarketType::LinearSwap, "XBTUSDTM")] #[test_case(MarketType::InverseFuture, "XBTMZ22")] fn test_crawl_l2_snapshot(market_type: MarketType, symbol: &str) { test_crawl_restful!( crawl_l2_snapshot, EXCHANGE_NAME, market_type, symbol, MessageType::L2Snapshot ) } #[test_case(MarketType::Spot)] #[test_case(MarketType::InverseSwap)] #[test_case(MarketType::LinearSwap)] #[test_case(MarketType::InverseFuture)] fn test_crawl_l2_snapshot_without_symbol(market_type: MarketType) { test_crawl_restful_all_symbols!( crawl_l2_snapshot, EXCHANGE_NAME, market_type, MessageType::L2Snapshot ) } // #[test_case(MarketType::Spot, "BTC-USDT"; "ignore")] #[test_case(MarketType::InverseSwap, "XBTUSDM")] #[test_case(MarketType::LinearSwap, "XBTUSDTM")] #[test_case(MarketType::InverseFuture, "XBTMZ22")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_l3_snapshot(market_type: MarketType, symbol: &str) { test_crawl_restful!( crawl_l3_snapshot, EXCHANGE_NAME, market_type, symbol, MessageType::L3Snapshot ) } #[test_case(MarketType::Spot, "BTC-USDT")] #[test_case(MarketType::InverseSwap, "XBTUSDM")] #[test_case(MarketType::LinearSwap, "XBTUSDTM")] #[test_case(MarketType::InverseFuture, "XBTMZ22")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_ticker(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_ticker, EXCHANGE_NAME, market_type, symbol, MessageType::Ticker) } #[test_case(MarketType::Spot)] #[test_case(MarketType::InverseSwap)] #[test_case(MarketType::LinearSwap)] // #[test_case(MarketType::InverseFuture)] #[tokio::test(flavor = "multi_thread", worker_threads = 8)] async fn test_crawl_candlestick(market_type: MarketType) { gen_test_crawl_candlestick!(EXCHANGE_NAME, market_type) } ================================================ FILE: crypto-crawler/tests/mexc.rs ================================================ #[macro_use] mod utils; use test_case::test_case; use crypto_crawler::*; use crypto_market_type::MarketType; use crypto_msg_type::MessageType; use utils::parse; const EXCHANGE_NAME: &str = "mexc"; #[test_case(MarketType::Spot)] #[test_case(MarketType::InverseSwap)] #[test_case(MarketType::LinearSwap)] #[tokio::test(flavor = "multi_thread", worker_threads = 8)] async fn test_crawl_trade_all(market_type: MarketType) { test_all_symbols!(crawl_trade, EXCHANGE_NAME, market_type, MessageType::Trade) } #[test_case(MarketType::Spot, "BTC_USDT")] #[test_case(MarketType::LinearSwap, "BTC_USDT")] #[test_case(MarketType::InverseSwap, "BTC_USD")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_trade(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_trade, EXCHANGE_NAME, market_type, symbol, MessageType::Trade) } #[test_case(MarketType::Spot, "BTC_USDT")] #[test_case(MarketType::LinearSwap, "BTC_USDT")] #[test_case(MarketType::InverseSwap, "BTC_USD")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_l2_event(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_l2_event, EXCHANGE_NAME, market_type, symbol, MessageType::L2Event) } #[test_case(MarketType::Spot, "BTC_USDT")] #[test_case(MarketType::LinearSwap, "BTC_USDT")] #[test_case(MarketType::InverseSwap, "BTC_USD")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_l2_topk(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_l2_topk, EXCHANGE_NAME, market_type, symbol, MessageType::L2TopK) } #[test_case(MarketType::Spot, "BTC_USDT")] #[test_case(MarketType::LinearSwap, "BTC_USDT")] #[test_case(MarketType::InverseSwap, "BTC_USD")] fn test_crawl_l2_snapshot(market_type: MarketType, symbol: &str) { test_crawl_restful!( crawl_l2_snapshot, EXCHANGE_NAME, market_type, symbol, MessageType::L2Snapshot ) } #[test_case(MarketType::Spot)] #[test_case(MarketType::LinearSwap)] #[test_case(MarketType::InverseSwap)] fn test_crawl_l2_snapshot_without_symbol(market_type: MarketType) { test_crawl_restful_all_symbols!( crawl_l2_snapshot, EXCHANGE_NAME, market_type, MessageType::L2Snapshot ) } #[test_case(MarketType::LinearSwap, "BTC_USDT")] #[test_case(MarketType::InverseSwap, "BTC_USD")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_ticker(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_ticker, EXCHANGE_NAME, market_type, symbol, MessageType::Ticker) } #[test_case(MarketType::Spot)] #[test_case(MarketType::LinearSwap)] #[test_case(MarketType::InverseSwap)] #[tokio::test(flavor = "multi_thread", worker_threads = 8)] async fn test_crawl_candlestick(market_type: MarketType) { gen_test_crawl_candlestick!(EXCHANGE_NAME, market_type) } ================================================ FILE: crypto-crawler/tests/okx.rs ================================================ #[macro_use] mod utils; use test_case::test_case; use crypto_crawler::*; use crypto_market_type::MarketType; use crypto_msg_type::MessageType; use utils::parse; const EXCHANGE_NAME: &str = "okx"; #[test_case(MarketType::Spot)] #[test_case(MarketType::InverseFuture)] #[test_case(MarketType::LinearFuture)] #[test_case(MarketType::InverseSwap)] #[test_case(MarketType::LinearSwap)] #[test_case(MarketType::EuropeanOption)] #[tokio::test(flavor = "multi_thread", worker_threads = 8)] async fn test_crawl_trade_all(market_type: MarketType) { test_all_symbols!(crawl_trade, EXCHANGE_NAME, market_type, MessageType::Trade) } #[test_case(MarketType::Spot, "BTC-USDT")] #[test_case(MarketType::InverseFuture, "BTC-USD-221230")] #[test_case(MarketType::LinearFuture, "BTC-USDT-221230")] #[test_case(MarketType::InverseSwap, "BTC-USD-SWAP")] #[test_case(MarketType::LinearSwap, "BTC-USDT-SWAP")] // #[test_case(MarketType::EuropeanOption, "BTC-USD-221230-10000-P"; "ignore")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_trade(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_trade, EXCHANGE_NAME, market_type, symbol, MessageType::Trade) } #[test_case(MarketType::Spot, "BTC-USDT")] #[test_case(MarketType::InverseFuture, "BTC-USD-221230")] #[test_case(MarketType::LinearFuture, "BTC-USDT-221230")] #[test_case(MarketType::InverseSwap, "BTC-USD-SWAP")] #[test_case(MarketType::LinearSwap, "BTC-USDT-SWAP")] #[test_case(MarketType::EuropeanOption, "BTC-USD-221230-10000-P")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_l2_event(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_l2_event, EXCHANGE_NAME, market_type, symbol, MessageType::L2Event) } #[test_case(MarketType::Spot, "BTC-USDT")] #[test_case(MarketType::InverseFuture, "BTC-USD-221230")] #[test_case(MarketType::LinearFuture, "BTC-USDT-221230")] #[test_case(MarketType::InverseSwap, "BTC-USD-SWAP")] #[test_case(MarketType::LinearSwap, "BTC-USDT-SWAP")] #[test_case(MarketType::EuropeanOption, "BTC-USD-221230-10000-P")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_l2_topk(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_l2_topk, EXCHANGE_NAME, market_type, symbol, MessageType::L2TopK) } #[test_case(MarketType::Spot, "BTC-USDT")] #[test_case(MarketType::InverseFuture, "BTC-USD-221230")] #[test_case(MarketType::LinearFuture, "BTC-USDT-221230")] #[test_case(MarketType::InverseSwap, "BTC-USD-SWAP")] #[test_case(MarketType::LinearSwap, "BTC-USDT-SWAP")] #[test_case(MarketType::EuropeanOption, "BTC-USD-221230-10000-P")] fn test_crawl_l2_snapshot(market_type: MarketType, symbol: &str) { test_crawl_restful!( crawl_l2_snapshot, EXCHANGE_NAME, market_type, symbol, MessageType::L2Snapshot ) } #[test_case(MarketType::Spot)] #[test_case(MarketType::InverseFuture)] #[test_case(MarketType::LinearFuture)] #[test_case(MarketType::InverseSwap)] #[test_case(MarketType::LinearSwap)] #[test_case(MarketType::EuropeanOption)] fn test_crawl_l2_snapshot_without_symbol(market_type: MarketType) { test_crawl_restful_all_symbols!( crawl_l2_snapshot, EXCHANGE_NAME, market_type, MessageType::L2Snapshot ) } #[test_case(MarketType::InverseSwap, "BTC-USD-SWAP")] #[test_case(MarketType::LinearSwap, "BTC-USDT-SWAP")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_funding_rate(market_type: MarketType, symbol: &str) { test_one_symbol!( crawl_funding_rate, EXCHANGE_NAME, market_type, symbol, MessageType::FundingRate ) } #[test_case(MarketType::InverseFuture)] #[test_case(MarketType::LinearFuture)] #[test_case(MarketType::InverseSwap)] #[test_case(MarketType::LinearSwap)] #[test_case(MarketType::EuropeanOption)] fn test_crawl_open_interest(market_type: MarketType) { let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { crawl_open_interest(EXCHANGE_NAME, market_type, tx); }); let msg = rx.recv().unwrap(); assert_eq!(msg.exchange, EXCHANGE_NAME.to_string()); assert_eq!(msg.market_type, market_type); assert_eq!(msg.msg_type, MessageType::OpenInterest); assert!(parse(msg)); } #[test_case(MarketType::Spot, "BTC-USDT")] #[test_case(MarketType::InverseFuture, "BTC-USD-221230")] #[test_case(MarketType::LinearFuture, "BTC-USDT-221230")] #[test_case(MarketType::InverseSwap, "BTC-USD-SWAP")] #[test_case(MarketType::LinearSwap, "BTC-USDT-SWAP")] #[test_case(MarketType::EuropeanOption, "BTC-USD-221230-10000-P")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_ticker(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_ticker, EXCHANGE_NAME, market_type, symbol, MessageType::Ticker) } #[test_case(MarketType::Spot)] #[test_case(MarketType::InverseFuture)] #[test_case(MarketType::LinearFuture)] #[test_case(MarketType::InverseSwap)] #[test_case(MarketType::LinearSwap)] #[test_case(MarketType::EuropeanOption)] #[tokio::test(flavor = "multi_thread", worker_threads = 8)] async fn test_crawl_candlestick(market_type: MarketType) { gen_test_crawl_candlestick!(EXCHANGE_NAME, market_type) } // #[test_case(MarketType::Spot, "BTC-USDT")] // #[test_case(MarketType::InverseFuture, "BTC-USD-221230")] // #[test_case(MarketType::LinearFuture, "BTC-USDT-221230")] // #[test_case(MarketType::InverseSwap, "BTC-USD-SWAP")] // #[test_case(MarketType::LinearSwap, "BTC-USDT-SWAP")] // #[test_case(MarketType::EuropeanOption, "BTC-USD-221230-10000-P")] // fn test_subscribe_symbol(market_type: MarketType, symbol: &str) { // gen_test_subscribe_symbol!(EXCHANGE_NAME, market_type, symbol) // } ================================================ FILE: crypto-crawler/tests/utils/mod.rs ================================================ use crypto_crawler::Message; use crypto_market_type::MarketType; use crypto_msg_type::MessageType; pub(crate) fn parse(msg: Message) -> bool { let skipped_exchanges = vec!["bitget", "zb"]; if skipped_exchanges.contains(&msg.exchange.as_str()) { return true; } match msg.msg_type { MessageType::Trade => { crypto_msg_parser::parse_trade(&msg.exchange, msg.market_type, &msg.json).is_ok() } MessageType::L2Event => { match msg.market_type { // crypto-msg-parser doesn't support quanto contracts MarketType::QuantoSwap | MarketType::QuantoFuture => true, _ => crypto_msg_parser::parse_l2( &msg.exchange, msg.market_type, &msg.json, Some(msg.received_at as i64), ) .is_ok(), } } MessageType::FundingRate => crypto_msg_parser::parse_funding_rate( &msg.exchange, msg.market_type, &msg.json, Some(msg.received_at as i64), ) .is_ok(), _ => true, } } #[allow(unused_macros)] macro_rules! test_one_symbol { ($crawl_func:ident, $exchange:expr, $market_type:expr, $symbol:expr, $msg_type:expr) => {{ let (tx, rx) = std::sync::mpsc::channel(); let symbols = vec![$symbol.to_string()]; tokio::task::spawn(async move { $crawl_func($exchange, $market_type, Some(&symbols), tx).await; }); let msg = rx.recv().unwrap(); assert_eq!(msg.exchange, $exchange.to_string()); assert_eq!(msg.market_type, $market_type); assert_eq!(msg.msg_type, $msg_type); assert!(tokio::task::block_in_place(move || parse(msg))); }}; } #[allow(unused_macros)] macro_rules! test_all_symbols { ($crawl_func:ident, $exchange:expr, $market_type:expr, $msg_type:expr) => {{ let (tx, rx) = std::sync::mpsc::channel(); tokio::task::spawn(async move { $crawl_func($exchange, $market_type, None, tx).await; }); let msg = rx.recv().unwrap(); assert_eq!(msg.exchange, $exchange.to_string()); assert_eq!(msg.market_type, $market_type); assert_eq!(msg.msg_type, $msg_type); assert!(tokio::task::block_in_place(move || parse(msg))); }}; } #[allow(unused_macros)] macro_rules! test_crawl_restful { ($crawl_func:ident, $exchange:expr, $market_type:expr, $symbol:expr, $msg_type:expr) => {{ let (tx, rx) = std::sync::mpsc::channel(); let symbols = vec![$symbol.to_string()]; std::thread::spawn(move || { $crawl_func($exchange, $market_type, Some(&symbols), tx); }); let msg = rx.recv().unwrap(); assert_eq!(msg.exchange, $exchange.to_string()); assert_eq!(msg.market_type, $market_type); assert_eq!(msg.msg_type, $msg_type); assert!(parse(msg)); }}; } #[allow(unused_macros)] macro_rules! test_crawl_restful_all_symbols { ($crawl_func:ident, $exchange:expr, $market_type:expr, $msg_type:expr) => {{ let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { $crawl_func($exchange, $market_type, None, tx); }); let msg = rx.recv().unwrap(); assert_eq!(msg.exchange, $exchange.to_string()); assert_eq!(msg.market_type, $market_type); assert_eq!(msg.msg_type, $msg_type); assert!(parse(msg)); }}; } #[allow(unused_macros)] macro_rules! gen_test_crawl_candlestick { ($exchange:expr, $market_type:expr) => {{ let (tx, rx) = std::sync::mpsc::channel(); tokio::task::spawn(async move { crawl_candlestick($exchange, $market_type, None, tx).await; }); let msg = rx.recv().unwrap(); assert_eq!(msg.exchange, $exchange.to_string()); assert_eq!(msg.market_type, $market_type); assert_eq!(msg.msg_type, MessageType::Candlestick); assert!(tokio::task::block_in_place(move || parse(msg))); }}; } #[allow(unused_macros)] macro_rules! gen_test_subscribe_symbol { ($exchange:expr, $market_type:expr, $symbol:expr) => {{ let (tx, rx) = std::sync::mpsc::channel(); tokio::task::spawn(async move { let msg_types = vec![MessageType::Trade, MessageType::L2Event]; subscribe_symbol($exchange, $market_type, $symbol, &msg_types, tx).await; }); let mut messages = Vec::new(); for msg in rx { messages.push(msg); break; } assert!(!messages.is_empty()); }}; } ================================================ FILE: crypto-crawler/tests/zb.rs ================================================ #[macro_use] mod utils; use test_case::test_case; use crypto_crawler::*; use crypto_market_type::MarketType; use crypto_msg_type::MessageType; use utils::parse; const EXCHANGE_NAME: &str = "zb"; #[test_case(MarketType::Spot)] #[test_case(MarketType::LinearSwap)] #[tokio::test(flavor = "multi_thread", worker_threads = 8)] async fn test_crawl_trade_all(market_type: MarketType) { test_all_symbols!(crawl_trade, EXCHANGE_NAME, market_type, MessageType::Trade) } #[test_case(MarketType::Spot, "btc_usdt")] #[test_case(MarketType::LinearSwap, "BTC_USDT")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_trade(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_trade, EXCHANGE_NAME, market_type, symbol, MessageType::Trade) } // #[test_case(MarketType::Spot, "btc_usdt")] #[test_case(MarketType::LinearSwap, "BTC_USDT")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_l2_event(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_l2_event, EXCHANGE_NAME, market_type, symbol, MessageType::L2Event) } #[test_case(MarketType::Spot, "btc_usdt")] #[test_case(MarketType::LinearSwap, "BTC_USDT")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_l2_topk(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_l2_topk, EXCHANGE_NAME, market_type, symbol, MessageType::L2TopK) } #[test_case(MarketType::Spot, "btc_usdt")] #[test_case(MarketType::LinearSwap, "BTC_USDT")] fn test_crawl_l2_snapshot(market_type: MarketType, symbol: &str) { test_crawl_restful!( crawl_l2_snapshot, EXCHANGE_NAME, market_type, symbol, MessageType::L2Snapshot ) } #[test_case(MarketType::Spot)] #[test_case(MarketType::LinearSwap)] fn test_crawl_l2_snapshot_without_symbol(market_type: MarketType) { test_crawl_restful_all_symbols!( crawl_l2_snapshot, EXCHANGE_NAME, market_type, MessageType::L2Snapshot ) } #[test_case(MarketType::Spot, "btc_usdt")] #[test_case(MarketType::LinearSwap, "BTC_USDT")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_ticker(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_ticker, EXCHANGE_NAME, market_type, symbol, MessageType::Ticker) } #[test_case(MarketType::Spot)] #[test_case(MarketType::LinearSwap)] #[tokio::test(flavor = "multi_thread", worker_threads = 8)] async fn test_crawl_candlestick(market_type: MarketType) { gen_test_crawl_candlestick!(EXCHANGE_NAME, market_type) } ================================================ FILE: crypto-crawler/tests/zbg.rs ================================================ #[macro_use] mod utils; use test_case::test_case; use crypto_crawler::*; use crypto_market_type::MarketType; use crypto_msg_type::MessageType; use utils::parse; const EXCHANGE_NAME: &str = "zbg"; #[test_case(MarketType::Spot)] #[tokio::test(flavor = "multi_thread", worker_threads = 8)] async fn test_crawl_trade_all(market_type: MarketType) { test_all_symbols!(crawl_trade, EXCHANGE_NAME, market_type, MessageType::Trade) } #[test_case(MarketType::Spot, "btc_usdt")] // #[test_case(MarketType::InverseSwap, "BTC_USD-R")] // #[test_case(MarketType::LinearSwap, "BTC_USDT")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_trade(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_trade, EXCHANGE_NAME, market_type, symbol, MessageType::Trade) } #[test_case(MarketType::Spot, "btc_usdt")] // #[test_case(MarketType::InverseSwap, "BTC_USD-R")] // #[test_case(MarketType::LinearSwap, "BTC_USDT")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_l2_event(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_l2_event, EXCHANGE_NAME, market_type, symbol, MessageType::L2Event) } #[test_case(MarketType::Spot, "btc_usdt")] // #[test_case(MarketType::InverseSwap, "BTC_USD-R")] // #[test_case(MarketType::LinearSwap, "BTC_USDT")] fn test_crawl_l2_snapshot(market_type: MarketType, symbol: &str) { test_crawl_restful!( crawl_l2_snapshot, EXCHANGE_NAME, market_type, symbol, MessageType::L2Snapshot ) } #[test_case(MarketType::Spot)] #[test_case(MarketType::InverseSwap)] #[test_case(MarketType::LinearSwap)] fn test_crawl_l2_snapshot_without_symbol(market_type: MarketType) { test_crawl_restful_all_symbols!( crawl_l2_snapshot, EXCHANGE_NAME, market_type, MessageType::L2Snapshot ) } #[test_case(MarketType::Spot, "btc_usdt")] // #[test_case(MarketType::InverseSwap, "BTC_USD-R")] // #[test_case(MarketType::LinearSwap, "BTC_USDT")] #[tokio::test(flavor = "multi_thread")] async fn test_crawl_ticker(market_type: MarketType, symbol: &str) { test_one_symbol!(crawl_ticker, EXCHANGE_NAME, market_type, symbol, MessageType::Ticker) } #[test_case(MarketType::Spot)] // #[test_case(MarketType::InverseSwap)] // #[test_case(MarketType::LinearSwap)] #[tokio::test(flavor = "multi_thread", worker_threads = 8)] async fn test_crawl_candlestick(market_type: MarketType) { gen_test_crawl_candlestick!(EXCHANGE_NAME, market_type) } ================================================ FILE: crypto-market-type/Cargo.toml ================================================ [package] name = "crypto-market-type" version = "1.1.5" authors = ["soulmachine "] edition = "2021" description = "Cryptocurrenty market type" license = "Apache-2.0" repository = "https://github.com/crypto-crawler/crypto-crawler-rs/tree/main/crypto-market-type" keywords = ["cryptocurrency", "blockchain", "trading"] [dependencies] serde = { version = "1", features = ["derive"] } strum = "0.24" strum_macros = "0.24" ================================================ FILE: crypto-market-type/include/crypto_market_type.h ================================================ /* Licensed under Apache-2.0 */ #ifndef CRYPTO_MARKET_TYPE_H_ #define CRYPTO_MARKET_TYPE_H_ /** * Market type. * * * In spot market, cryptocurrencies are traded for immediate delivery, see * https://en.wikipedia.org/wiki/Spot_market. * * In futures market, delivery is set at a specified time in the future, see * https://en.wikipedia.org/wiki/Futures_exchange. * * Swap market is a variant of futures market with no expiry date. * * ## Margin * * A market can have margin enabled or disabled. * * * All contract markets are margin enabled, including future, swap and option. * * Most spot markets don't have margin enabled, only a few exchanges have spot * market with margin enabled. * * ## Linear VS. Inverse * * A market can be inverse or linear. * * Linear means USDT-margined, i.e., you can use USDT as collateral * * Inverse means coin-margined, i.e., you can use BTC as collateral. * * Spot market is always linear. * * **Margin and Inverse are orthogonal.** */ typedef enum { Unknown, Spot, LinearFuture, InverseFuture, LinearSwap, InverseSwap, AmericanOption, EuropeanOption, QuantoFuture, QuantoSwap, Move, BVOL, } MarketType; #endif /* CRYPTO_MARKET_TYPE_H_ */ ================================================ FILE: crypto-market-type/src/lib.rs ================================================ use serde::{Deserialize, Serialize}; use strum_macros::{Display, EnumString}; /// Market type. /// /// * In spot market, cryptocurrencies are traded for immediate delivery, see https://en.wikipedia.org/wiki/Spot_market. /// * In futures market, delivery is set at a specified time in the future, see https://en.wikipedia.org/wiki/Futures_exchange. /// * Swap market is a variant of futures market with no expiry date. /// /// ## Margin /// /// A market can have margin enabled or disabled. /// /// * All contract markets are margin enabled, including future, swap and /// option. /// * Most spot markets don't have margin enabled, only a few exchanges have /// spot market with margin enabled. /// /// ## Linear VS. Inverse /// /// A market can be inverse or linear. /// * Linear means USDT-margined, i.e., you can use USDT as collateral /// * Inverse means coin-margined, i.e., you can use BTC as collateral. /// * Spot market is always linear. /// /// **Margin and Inverse are orthogonal.** #[repr(C)] #[derive(Copy, Clone, Serialize, Deserialize, Display, Debug, EnumString, PartialEq, Hash, Eq)] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum MarketType { Unknown, Spot, LinearFuture, InverseFuture, LinearSwap, InverseSwap, AmericanOption, EuropeanOption, QuantoFuture, QuantoSwap, Move, #[serde(rename = "bvol")] #[allow(clippy::upper_case_acronyms)] BVOL, } /// Get market types of a cryptocurrency exchange. pub fn get_market_types(exchange: &str) -> Vec { match exchange { "binance" => vec![ MarketType::Spot, MarketType::LinearFuture, MarketType::InverseFuture, MarketType::LinearSwap, MarketType::InverseSwap, // MarketType::EuropeanOption, // binance has shutdown option markets. ], "bitfinex" => vec![MarketType::Spot, MarketType::LinearSwap], "bitget" => vec![ MarketType::Spot, MarketType::InverseSwap, /* TODO: Bitget's coin-margined swap market is a kind of * mixed contract */ MarketType::LinearSwap, MarketType::InverseFuture, ], "bithumb" => vec![MarketType::Spot], // BitMEX only handles Bitcoin. All profit and loss is in Bitcoin "bitmex" => vec![ MarketType::Spot, MarketType::LinearSwap, MarketType::InverseSwap, MarketType::QuantoSwap, MarketType::LinearFuture, MarketType::InverseFuture, MarketType::QuantoFuture, ], "bitstamp" => vec![MarketType::Spot], "bitz" => vec![MarketType::Spot, MarketType::InverseSwap, MarketType::LinearSwap], "bybit" => vec![MarketType::InverseSwap, MarketType::LinearSwap, MarketType::InverseFuture], "coinbase_pro" => vec![MarketType::Spot], // Deribit only accepts Bitcoin as funds to deposit. "deribit" => vec![ MarketType::InverseFuture, MarketType::InverseSwap, MarketType::EuropeanOption, // inverse ], "dydx" => vec![MarketType::LinearSwap], "ftx" => vec![ MarketType::Spot, MarketType::LinearFuture, MarketType::LinearSwap, MarketType::Move, MarketType::BVOL, ], "gate" => vec![ MarketType::Spot, MarketType::InverseFuture, MarketType::LinearFuture, MarketType::InverseSwap, MarketType::LinearSwap, ], "huobi" => vec![ MarketType::Spot, MarketType::InverseFuture, MarketType::LinearSwap, MarketType::InverseSwap, // MarketType::EuropeanOption, ], "kraken" => vec![MarketType::Spot, MarketType::InverseFuture, MarketType::InverseSwap], "kucoin" => vec![ MarketType::Spot, MarketType::LinearSwap, MarketType::InverseSwap, MarketType::InverseFuture, ], "mxc" | "mexc" => vec![MarketType::Spot, MarketType::LinearSwap, MarketType::InverseSwap], "okex" | "okx" => vec![ MarketType::Spot, MarketType::LinearFuture, MarketType::InverseFuture, MarketType::LinearSwap, MarketType::InverseSwap, MarketType::EuropeanOption, ], "zb" => vec![MarketType::Spot, MarketType::LinearSwap], "zbg" => vec![MarketType::Spot, MarketType::InverseSwap, MarketType::LinearSwap], _ => panic!("Unknown exchange {exchange}"), } } ================================================ FILE: crypto-markets/Cargo.toml ================================================ [package] name = "crypto-markets" version = "1.3.11" authors = ["soulmachine "] edition = "2021" description = "Fetch trading markets from a cryptocurrency exchange" license = "Apache-2.0" repository = "https://github.com/crypto-crawler/crypto-crawler-rs/tree/main/crypto-markets" keywords = ["cryptocurrency", "blockchain", "trading"] [dependencies] chrono = "0.4.24" crypto-market-type = "1.1.5" crypto-pair = "2.3.13" reqwest = { version = "0.11.14", features = ["blocking", "gzip", "socks"] } serde = { version = "1.0.157", features = ["derive"] } serde_json = "1.0.94" [dev_dependencies] crypto-contract-value = "1.7.13" test-case = "1" ================================================ FILE: crypto-markets/README.md ================================================ # crypto-markets [![](https://img.shields.io/github/workflow/status/crypto-crawler/crypto-crawler-rs/CI/main)](https://github.com/crypto-crawler/crypto-crawler-rs/actions?query=branch%3Amain) [![](https://img.shields.io/crates/v/crypto-markets.svg)](https://crates.io/crates/crypto-markets) [![](https://docs.rs/crypto-markets/badge.svg)](https://docs.rs/crypto-markets) ========== Fetch trading markets from a cryptocurrency exchange. ## Example ```rust use crypto_markets::{fetch_markets, MarketType}; fn main() { let markets = fetch_markets("Binance", MarketType::Spot).unwrap(); println!("{}", serde_json::to_string_pretty(&markets).unwrap()) } ``` ================================================ FILE: crypto-markets/src/error.rs ================================================ use std::{error::Error as StdError, fmt}; pub(crate) type Result = std::result::Result; #[derive(Debug)] pub struct Error(pub String); impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.0) } } impl StdError for Error {} impl From for Error { fn from(err: reqwest::Error) -> Self { Error(err.to_string()) } } impl From for Error { fn from(err: serde_json::Error) -> Self { Error(err.to_string()) } } ================================================ FILE: crypto-markets/src/exchanges/binance/binance_inverse.rs ================================================ use super::utils::{binance_http_get, parse_filter}; use crate::{error::Result, market::*, Market, MarketType}; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; #[derive(Serialize, Deserialize)] struct BinanceResponse { symbols: Vec, } #[derive(Serialize, Deserialize)] #[allow(non_snake_case)] struct FutureMarket { symbol: String, pair: String, contractType: String, deliveryDate: u64, onboardDate: u64, contractStatus: String, contractSize: f64, marginAsset: String, maintMarginPercent: String, requiredMarginPercent: String, baseAsset: String, quoteAsset: String, pricePrecision: i64, quantityPrecision: i64, baseAssetPrecision: i64, quotePrecision: i64, equalQtyPrecision: i64, triggerProtect: String, underlyingType: String, filters: Vec>, orderTypes: Vec, timeInForce: Vec, #[serde(flatten)] extra: HashMap, } // see fn fetch_inverse_markets_raw() -> Result> { let txt = binance_http_get("https://dapi.binance.com/dapi/v1/exchangeInfo")?; let resp = serde_json::from_str::>(&txt)?; let symbols: Vec = resp.symbols.into_iter().filter(|m| m.contractStatus == "TRADING").collect(); Ok(symbols) } pub(super) fn fetch_inverse_future_symbols() -> Result> { let symbols = fetch_inverse_markets_raw()? .into_iter() .filter(|m| m.contractType != "PERPETUAL") .map(|m| m.symbol) .collect::>(); Ok(symbols) } pub(super) fn fetch_inverse_swap_symbols() -> Result> { let symbols = fetch_inverse_markets_raw()? .into_iter() .filter(|m| m.contractType == "PERPETUAL") .map(|m| m.symbol) .collect::>(); Ok(symbols) } fn fetch_future_markets_internal() -> Result> { let raw_markets = fetch_inverse_markets_raw()?; let markets = raw_markets .into_iter() .map(|m| { Market { exchange: "binance".to_string(), market_type: if m.contractType == "PERPETUAL" { MarketType::InverseSwap } else { MarketType::InverseFuture }, symbol: m.symbol.clone(), base_id: m.baseAsset.clone(), quote_id: m.quoteAsset.clone(), settle_id: Some(m.marginAsset.clone()), base: m.baseAsset.clone(), quote: m.quoteAsset.clone(), settle: Some(m.marginAsset.clone()), active: m.contractStatus == "TRADING", margin: true, // see https://www.binance.com/en/fee/futureFee fees: Fees { maker: 0.00015, taker: 0.0004 }, precision: Precision { tick_size: 1.0 / (10_i64.pow(m.pricePrecision as u32) as f64), lot_size: 1.0 / (10_i64.pow(m.quantityPrecision as u32) as f64), }, quantity_limit: Some(QuantityLimit { min: parse_filter(&m.filters, "LOT_SIZE", "minQty").parse::().ok(), max: Some( parse_filter(&m.filters, "LOT_SIZE", "maxQty").parse::().unwrap(), ), notional_min: None, notional_max: None, }), contract_value: Some(m.contractSize), delivery_date: if m.contractType == "PERPETUAL" { None } else { Some(m.deliveryDate) }, info: serde_json::to_value(&m).unwrap().as_object().unwrap().clone(), } }) .collect::>(); Ok(markets) } pub(super) fn fetch_inverse_future_markets() -> Result> { let markets = fetch_future_markets_internal()? .into_iter() .filter(|m| m.market_type == MarketType::InverseFuture) .collect(); Ok(markets) } pub(super) fn fetch_inverse_swap_markets() -> Result> { let markets = fetch_future_markets_internal()? .into_iter() .filter(|m| m.market_type == MarketType::InverseSwap) .collect(); Ok(markets) } ================================================ FILE: crypto-markets/src/exchanges/binance/binance_linear.rs ================================================ use super::utils::{binance_http_get, parse_filter}; use crate::{error::Result, market::*, Market, MarketType}; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; #[derive(Serialize, Deserialize)] struct BinanceResponse { symbols: Vec, } #[derive(Serialize, Deserialize)] #[allow(non_snake_case)] struct LinearSwapMarket { symbol: String, pair: String, contractType: String, deliveryDate: u64, onboardDate: u64, status: String, maintMarginPercent: String, requiredMarginPercent: String, baseAsset: String, quoteAsset: String, marginAsset: String, pricePrecision: i64, quantityPrecision: i64, baseAssetPrecision: i64, quotePrecision: i64, underlyingType: String, triggerProtect: String, filters: Vec>, orderTypes: Vec, timeInForce: Vec, #[serde(flatten)] extra: HashMap, } // see fn fetch_linear_markets_raw() -> Result> { let txt = binance_http_get("https://fapi.binance.com/fapi/v1/exchangeInfo")?; let resp = serde_json::from_str::>(&txt)?; let symbols: Vec = resp.symbols.into_iter().filter(|m| m.status == "TRADING").collect(); Ok(symbols) } pub(super) fn fetch_linear_swap_symbols() -> Result> { let symbols = fetch_linear_markets_raw()? .into_iter() .filter(|m| m.contractType == "PERPETUAL") .map(|m| m.symbol) .collect::>(); Ok(symbols) } pub(super) fn fetch_linear_future_symbols() -> Result> { let symbols = fetch_linear_markets_raw()? .into_iter() .filter(|m| m.contractType != "PERPETUAL") .map(|m| m.symbol) .collect::>(); Ok(symbols) } fn fetch_linear_markets() -> Result> { let raw_markets = fetch_linear_markets_raw()?; let markets = raw_markets .into_iter() .map(|m| { Market { exchange: "binance".to_string(), market_type: if m.contractType == "PERPETUAL" { MarketType::LinearSwap } else { MarketType::LinearFuture }, symbol: m.symbol.clone(), base_id: m.baseAsset.clone(), quote_id: m.quoteAsset.clone(), settle_id: Some(m.marginAsset.clone()), base: m.baseAsset.clone(), quote: m.quoteAsset.clone(), settle: Some(m.marginAsset.clone()), active: m.status == "TRADING", margin: true, // see https://www.binance.com/en/fee/futureFee fees: Fees { maker: 0.0002, taker: 0.0004 }, precision: Precision { tick_size: 1.0 / (10_i64.pow(m.pricePrecision as u32) as f64), lot_size: 1.0 / (10_i64.pow(m.quantityPrecision as u32) as f64), }, quantity_limit: Some(QuantityLimit { min: parse_filter(&m.filters, "LOT_SIZE", "minQty").parse::().ok(), max: Some( parse_filter(&m.filters, "LOT_SIZE", "maxQty").parse::().unwrap(), ), notional_min: None, notional_max: None, }), contract_value: Some(1.0), delivery_date: if m.contractType == "PERPETUAL" { None } else { Some(m.deliveryDate) }, info: serde_json::to_value(&m).unwrap().as_object().unwrap().clone(), } }) .collect::>(); Ok(markets) } pub(super) fn fetch_linear_swap_markets() -> Result> { let markets = fetch_linear_markets()?; let swap_markets = markets.into_iter().filter(|m| m.market_type == MarketType::LinearSwap).collect(); Ok(swap_markets) } pub(super) fn fetch_linear_future_markets() -> Result> { let markets = fetch_linear_markets()?; let future_markets = markets.into_iter().filter(|m| m.market_type == MarketType::LinearFuture).collect(); Ok(future_markets) } ================================================ FILE: crypto-markets/src/exchanges/binance/binance_option.rs ================================================ use super::utils::binance_http_get; use crate::{error::Result, market::*, Market, MarketType}; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; #[derive(Serialize, Deserialize)] struct BinanceResponse { symbols: Vec, } #[derive(Serialize, Deserialize)] #[allow(non_snake_case)] struct OptionMarket { id: i64, contractId: i64, underlying: String, quoteAsset: String, symbol: String, unit: String, minQty: String, maxQty: String, priceScale: i64, quantityScale: i64, side: String, makerFeeRate: String, takerFeeRate: String, expiryDate: u64, #[serde(flatten)] extra: HashMap, } fn fetch_option_markets_raw() -> Result> { #[derive(Serialize, Deserialize)] #[allow(non_snake_case)] struct OptionData { timezone: String, serverTime: i64, optionContracts: Vec, optionAssets: Vec, optionSymbols: Vec, } #[derive(Serialize, Deserialize)] #[allow(non_snake_case)] struct BinanceOptionResponse { code: i64, msg: String, data: OptionData, } let txt = binance_http_get("https://vapi.binance.com/vapi/v1/exchangeInfo")?; let resp = serde_json::from_str::(&txt)?; Ok(resp.data.optionSymbols) } pub(super) fn fetch_option_symbols() -> Result> { let symbols = fetch_option_markets_raw()?.into_iter().map(|m| m.symbol).collect::>(); Ok(symbols) } pub(super) fn fetch_option_markets() -> Result> { let raw_markets = fetch_option_markets_raw()?; let markets = raw_markets .into_iter() .map(|m| { let base_currency = m.underlying.strip_suffix(m.quoteAsset.as_str()).unwrap(); Market { exchange: "binance".to_string(), market_type: MarketType::EuropeanOption, symbol: m.symbol.clone(), base_id: base_currency.to_string(), quote_id: m.quoteAsset.clone(), settle_id: Some(m.quoteAsset.clone()), base: base_currency.to_string(), quote: m.quoteAsset.clone(), settle: Some(m.quoteAsset.clone()), active: true, margin: true, // see https://www.binance.com/en/fee/optionFee fees: Fees { maker: m.makerFeeRate.parse::().unwrap(), taker: m.takerFeeRate.parse::().unwrap(), }, precision: Precision { tick_size: 1.0 / (10_i64.pow(m.priceScale as u32) as f64), lot_size: 1.0 / (10_i64.pow(m.quantityScale as u32) as f64), }, quantity_limit: Some(QuantityLimit { min: m.minQty.parse::().ok(), max: Some(m.maxQty.parse::().unwrap()), notional_min: None, notional_max: None, }), contract_value: Some(1.0), delivery_date: Some(m.expiryDate), info: serde_json::to_value(&m).unwrap().as_object().unwrap().clone(), } }) .collect::>(); Ok(markets) } ================================================ FILE: crypto-markets/src/exchanges/binance/binance_spot.rs ================================================ use super::utils::{binance_http_get, parse_filter}; use crate::{error::Result, market::*, Market, MarketType}; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; #[derive(Serialize, Deserialize)] struct BinanceResponse { symbols: Vec, } #[derive(Serialize, Deserialize)] #[allow(non_snake_case)] struct SpotMarket { symbol: String, status: String, baseAsset: String, baseAssetPrecision: i32, quoteAsset: String, quotePrecision: i32, quoteAssetPrecision: i32, isSpotTradingAllowed: bool, isMarginTradingAllowed: bool, filters: Vec>, #[serde(flatten)] extra: HashMap, } // see fn fetch_spot_markets_raw() -> Result> { let txt = binance_http_get("https://api.binance.com/api/v3/exchangeInfo")?; let resp = serde_json::from_str::>(&txt)?; Ok(resp.symbols.into_iter().filter(|s| s.symbol != "123456").collect()) } pub(super) fn fetch_spot_symbols() -> Result> { let symbols = fetch_spot_markets_raw()? .into_iter() .filter(|m| m.status == "TRADING" && m.isSpotTradingAllowed) .map(|m| m.symbol) .collect::>(); Ok(symbols) } pub(super) fn fetch_spot_markets() -> Result> { let raw_markets = fetch_spot_markets_raw()?; let markets = raw_markets .into_iter() .map(|m| { Market { exchange: "binance".to_string(), market_type: MarketType::Spot, symbol: m.symbol.clone(), base_id: m.baseAsset.clone(), quote_id: m.quoteAsset.clone(), settle_id: None, base: m.baseAsset.clone(), quote: m.quoteAsset.clone(), settle: None, active: m.status == "TRADING" && m.isSpotTradingAllowed, margin: m.isMarginTradingAllowed, // see https://www.binance.com/en/fee/trading fees: Fees { maker: 0.001, taker: 0.001 }, precision: Precision { tick_size: parse_filter(&m.filters, "PRICE_FILTER", "tickSize") .parse::() .unwrap(), lot_size: parse_filter(&m.filters, "LOT_SIZE", "stepSize") .parse::() .unwrap(), }, quantity_limit: Some(QuantityLimit { min: parse_filter(&m.filters, "LOT_SIZE", "minQty").parse::().ok(), max: Some( parse_filter(&m.filters, "LOT_SIZE", "maxQty").parse::().unwrap(), ), notional_min: None, notional_max: None, }), contract_value: None, delivery_date: None, info: serde_json::to_value(&m).unwrap().as_object().unwrap().clone(), } }) .collect::>(); Ok(markets) } ================================================ FILE: crypto-markets/src/exchanges/binance/mod.rs ================================================ pub(super) mod binance_inverse; pub(super) mod binance_linear; pub(super) mod binance_option; pub(super) mod binance_spot; mod utils; use crate::{error::Result, Market, MarketType}; pub(crate) fn fetch_symbols(market_type: MarketType) -> Result> { match market_type { MarketType::Spot => binance_spot::fetch_spot_symbols(), MarketType::LinearFuture => binance_linear::fetch_linear_future_symbols(), MarketType::InverseFuture => binance_inverse::fetch_inverse_future_symbols(), MarketType::LinearSwap => binance_linear::fetch_linear_swap_symbols(), MarketType::InverseSwap => binance_inverse::fetch_inverse_swap_symbols(), MarketType::EuropeanOption => binance_option::fetch_option_symbols(), _ => panic!("Unsupported market_type: {market_type}"), } } pub(crate) fn fetch_markets(market_type: MarketType) -> Result> { match market_type { MarketType::Spot => binance_spot::fetch_spot_markets(), MarketType::LinearFuture => binance_linear::fetch_linear_future_markets(), MarketType::InverseFuture => binance_inverse::fetch_inverse_future_markets(), MarketType::LinearSwap => binance_linear::fetch_linear_swap_markets(), MarketType::InverseSwap => binance_inverse::fetch_inverse_swap_markets(), MarketType::EuropeanOption => binance_option::fetch_option_markets(), _ => panic!("Unsupported market_type: {market_type}"), } } ================================================ FILE: crypto-markets/src/exchanges/binance/utils.rs ================================================ use super::super::utils::http_get; use crate::error::{Error, Result}; use serde_json::Value; use std::collections::HashMap; fn check_code_in_body(resp: String) -> Result { let obj = serde_json::from_str::>(&resp); if obj.is_err() { return Ok(resp); } match obj.unwrap().get("code") { Some(code) => { if code.as_i64().unwrap() != 0 { Err(Error(resp)) } else { Ok(resp) } } None => Ok(resp), } } pub(super) fn binance_http_get(url: &str) -> Result { let ret = http_get(url, None); match ret { Ok(resp) => check_code_in_body(resp), Err(_) => ret, } } pub(super) fn parse_filter<'a>( filters: &'a [HashMap], filter_type: &'a str, field: &'static str, ) -> &'a str { filters.iter().find(|x| x["filterType"] == filter_type).unwrap()[field].as_str().unwrap() } ================================================ FILE: crypto-markets/src/exchanges/bitfinex.rs ================================================ use std::collections::HashMap; use super::utils::http_get; use crate::{ error::Result, market::{Fees, Precision, QuantityLimit}, Market, MarketType, }; use serde::{Deserialize, Serialize}; use serde_json::Value; pub(crate) fn fetch_symbols(market_type: MarketType) -> Result> { match market_type { MarketType::Spot => fetch_spot_symbols(), MarketType::LinearSwap => fetch_linear_swap_symbols(), _ => panic!("Unsupported market_type: {market_type}"), } } #[derive(Serialize, Deserialize)] struct RawMarket { pair: String, price_precision: i64, initial_margin: String, minimum_margin: String, maximum_order_size: String, minimum_order_size: String, expiration: String, margin: bool, #[serde(flatten)] extra: HashMap, } fn fetch_raw_markets() -> Result> { // can NOT use v2 API due to https://github.com/bitfinexcom/bitfinex-api-py/issues/95 let text = http_get("https://api.bitfinex.com/v1/symbols_details", None)?; let markets = serde_json::from_str::>(&text)?; let markets = markets.into_iter().filter(|m| !m.pair.starts_with("test")).collect::>(); Ok(markets) } pub(crate) fn fetch_markets(market_type: MarketType) -> Result> { let raw_markets = fetch_raw_markets()?; let raw_markets: Vec = match market_type { MarketType::Spot => raw_markets.into_iter().filter(|x| !x.pair.ends_with("f0")).collect(), MarketType::LinearSwap => { raw_markets.into_iter().filter(|x| x.pair.ends_with("f0")).collect() } _ => panic!("Unsupported market_type: {market_type}"), }; let markets: Vec = raw_markets .into_iter() .map(|m| { let symbol = m.pair.to_uppercase(); let pair = crypto_pair::normalize_pair(&symbol, "bitfinex").unwrap(); let (base, quote) = { let v: Vec<&str> = pair.split('/').collect(); (v[0].to_string(), v[1].to_string()) }; let (base_id, quote_id) = if symbol.contains(':') { let v: Vec<&str> = symbol.split(':').collect(); (v[0].to_string(), v[1].to_string()) } else { (symbol[..(symbol.len() - 3)].to_string(), symbol[(symbol.len() - 3)..].to_string()) }; Market { exchange: "bitfinex".to_string(), market_type, symbol: format!("t{symbol}"), base_id, quote_id: quote_id.clone(), settle_id: if market_type == MarketType::LinearSwap { Some(quote_id) } else { None }, base, quote: quote.clone(), settle: if market_type == MarketType::LinearSwap { Some(quote) } else { None }, active: true, margin: m.margin, // see https://www.bitfinex.com/fees fees: if market_type == MarketType::Spot { Fees { maker: 0.001, taker: 0.002 } } else { Fees { maker: -0.0002, taker: 0.00075 } }, precision: Precision { tick_size: 1.0 / (10_i64.pow(m.price_precision as u32) as f64), lot_size: 1.0 / (10_i64.pow(8_u32) as f64), }, quantity_limit: Some(QuantityLimit { min: m.minimum_order_size.parse::().ok(), max: Some(m.maximum_order_size.parse::().unwrap()), notional_min: None, notional_max: None, }), contract_value: if market_type == MarketType::Spot { None } else { Some(1.0) }, delivery_date: None, info: serde_json::to_value(&m).unwrap().as_object().unwrap().clone(), } }) .collect(); Ok(markets) } // see fn fetch_spot_symbols() -> Result> { let text = http_get("https://api-pub.bitfinex.com/v2/conf/pub:list:pair:exchange", None)?; let pairs = serde_json::from_str::>>(&text)?; let symbols = pairs[0] .iter() .filter(|x| !x.starts_with("TEST")) .map(|p| format!("t{p}")) .collect::>(); Ok(symbols) } // see fn fetch_linear_swap_symbols() -> Result> { let text = http_get("https://api-pub.bitfinex.com/v2/conf/pub:list:pair:futures", None)?; let pairs = serde_json::from_str::>>(&text)?; let symbols = pairs[0] .iter() .filter(|x| !x.starts_with("TEST")) .map(|p| format!("t{p}")) .collect::>(); Ok(symbols) } #[cfg(test)] mod tests { use serde_json::Value; use super::{ super::utils::http_get, fetch_linear_swap_symbols, fetch_raw_markets, fetch_spot_symbols, }; use crate::error::Result; fn _fetch_symbols(url: &str) -> Result> { let text = http_get(url, None)?; let arr = serde_json::from_str::>(&text)?; let arr = serde_json::from_value::>(arr[0].clone())?; let symbols = arr .iter() .map(|p| format!("t{}", p[0].as_str().unwrap())) .filter(|x| !x.starts_with("tTEST")) .collect::>(); Ok(symbols) } fn _fetch_spot_symbols() -> Result> { _fetch_symbols("https://api-pub.bitfinex.com/v2/conf/pub:info:pair") } fn _fetch_linear_swap_symbols() -> Result> { _fetch_symbols("https://api-pub.bitfinex.com/v2/conf/pub:info:pair:futures") } #[test] fn test_spot_symbols() { let mut symbols1 = _fetch_spot_symbols().unwrap(); let symbols2 = fetch_spot_symbols().unwrap(); assert_eq!(symbols1, symbols2); let mut symbols3: Vec = fetch_raw_markets() .unwrap() .into_iter() .map(|m| format!("t{}", m.pair.to_uppercase())) .filter(|x| !x.ends_with("F0")) .collect(); symbols1.sort(); symbols3.sort(); // assert_eq!(symbols1, symbols3); // sometimes symbols3 has extra // symbols that don't exist in symbols1 } #[test] fn test_linear_swap_symbols() { let mut symbols1 = _fetch_linear_swap_symbols().unwrap(); let symbols2 = fetch_linear_swap_symbols().unwrap(); assert_eq!(symbols1, symbols2); let mut symbols3: Vec = fetch_raw_markets() .unwrap() .into_iter() .map(|m| format!("t{}", m.pair.to_uppercase())) .filter(|x| x.ends_with("F0")) .collect(); symbols1.sort(); symbols3.sort(); assert_eq!(symbols1, symbols3); } } ================================================ FILE: crypto-markets/src/exchanges/bitget/bitget_spot.rs ================================================ use std::collections::HashMap; use super::{super::utils::http_get, EXCHANGE_NAME}; use crate::{ error::{Error, Result}, Fees, Market, Precision, QuantityLimit, }; use crypto_market_type::MarketType; use serde::{Deserialize, Serialize}; use serde_json::Value; // See https://bitgetlimited.github.io/apidoc/en/spot/#get-all-instruments #[derive(Clone, Serialize, Deserialize)] #[allow(non_snake_case)] struct SpotMarket { symbol: String, // symbol Id symbolName: String, // symbol name baseCoin: String, // Base coin quoteCoin: String, // Denomination coin minTradeAmount: String, // Min trading amount maxTradeAmount: String, // Max trading amount takerFeeRate: String, // Taker transaction fee rate makerFeeRate: String, // Maker transaction fee rate priceScale: String, // Maker transaction fee rate quantityScale: String, // Quantity scale status: String, // Status #[serde(flatten)] extra: HashMap, } #[derive(Serialize, Deserialize)] #[allow(non_snake_case)] struct Response { code: String, msg: String, data: Vec, requestTime: i64, #[serde(flatten)] extra: HashMap, } // See https://bitgetlimited.github.io/apidoc/en/spot/#get-all-instruments fn fetch_spot_markets_raw() -> Result> { let txt = http_get("https://api.bitget.com/api/spot/v1/public/products", None)?; let resp = serde_json::from_str::(&txt)?; if resp.msg != "success" { Err(Error(txt)) } else { let markets = resp .data .into_iter() // Ignored ETH_SPBL and BTC_SPBL for now because they're not tradable .filter(|x| x.status == "online" && x.symbol.ends_with("USDT_SPBL")) .collect::>(); Ok(markets) } } pub(super) fn fetch_spot_symbols() -> Result> { let markets = fetch_spot_markets_raw()?; let symbols: Vec = markets.into_iter().map(|m| m.symbol).collect(); Ok(symbols) } pub(super) fn fetch_spot_markets() -> Result> { let markets: Vec = fetch_spot_markets_raw()? .into_iter() .map(|m| Market { exchange: EXCHANGE_NAME.to_string(), market_type: MarketType::Spot, symbol: m.symbol.clone(), base_id: m.baseCoin.clone(), quote_id: m.quoteCoin.clone(), settle_id: None, base: m.baseCoin.clone(), quote: m.quoteCoin.clone(), settle: None, active: m.status == "online", margin: false, fees: Fees { maker: m.makerFeeRate.parse::().unwrap(), taker: m.takerFeeRate.parse::().unwrap(), }, precision: Precision { tick_size: 1.0 / (10_i64.pow(m.priceScale.parse::().unwrap()) as f64), lot_size: 1.0 / (10_i64.pow(m.quantityScale.parse::().unwrap()) as f64), }, quantity_limit: Some(QuantityLimit { min: m.minTradeAmount.parse::().ok(), max: if m.maxTradeAmount.parse::().unwrap() > 0.0 { Some(m.maxTradeAmount.parse::().unwrap()) } else { None }, notional_min: None, notional_max: None, }), contract_value: None, delivery_date: None, info: serde_json::to_value(&m).unwrap().as_object().unwrap().clone(), }) .collect(); Ok(markets) } ================================================ FILE: crypto-markets/src/exchanges/bitget/bitget_swap.rs ================================================ use std::collections::HashMap; use super::{super::utils::http_get, EXCHANGE_NAME}; use crate::{ error::{Error, Result}, Fees, Market, Precision, QuantityLimit, }; use chrono::DateTime; use crypto_market_type::MarketType; use serde::{Deserialize, Serialize}; use serde_json::Value; // See https://bitgetlimited.github.io/apidoc/en/mix/#get-all-symbols #[derive(Clone, Serialize, Deserialize)] #[allow(non_snake_case)] struct SwapMarket { symbol: String, // symbol Id baseCoin: String, // Base coin quoteCoin: String, // Denomination coin buyLimitPriceRatio: String, // Buy price limit ratio 1% sellLimitPriceRatio: String, // Sell price limit ratio 1% feeRateUpRatio: String, // Rate of increase in handling fee% takerFeeRate: String, // Taker fee rate% makerFeeRate: String, // Market fee rate% openCostUpRatio: String, // Percentage of increase in opening cost% supportMarginCoins: Vec, // Support margin currency minTradeNum: String, // Minimum number of openings priceEndStep: String, // Price step pricePlace: String, // Price decimal places volumePlace: String, // Number of decimal places #[serde(flatten)] extra: HashMap, } #[derive(Serialize, Deserialize)] #[allow(non_snake_case)] struct Response { code: String, msg: String, data: Vec, requestTime: i64, #[serde(flatten)] extra: HashMap, } // See https://bitgetlimited.github.io/apidoc/en/mix/#get-all-symbols // product_type: umcbl, LinearSwap; dmcbl, InverseSwap; fn fetch_swap_markets_raw(product_type: &str) -> Result> { let txt = http_get( format!("https://api.bitget.com/api/mix/v1/market/contracts?productType={product_type}") .as_str(), None, )?; let resp = serde_json::from_str::(&txt)?; if resp.msg != "success" { Err(Error(txt)) } else { Ok(resp.data) } } pub(super) fn fetch_inverse_swap_symbols() -> Result> { let symbols = fetch_swap_markets_raw("dmcbl")? .into_iter() .map(|m| m.symbol) .filter(|symbol| symbol.ends_with("_DMCBL")) .collect::>(); Ok(symbols) } pub(super) fn fetch_inverse_future_symbols() -> Result> { let symbols = fetch_swap_markets_raw("dmcbl")? .into_iter() .map(|m| m.symbol) .filter(|symbol| !symbol.ends_with("_DMCBL")) .collect::>(); Ok(symbols) } pub(super) fn fetch_linear_swap_symbols() -> Result> { // see https://bitgetlimited.github.io/apidoc/en/mix/#producttype let mut usdt_symbols = fetch_swap_markets_raw("umcbl")?.into_iter().map(|m| m.symbol).collect::>(); let usdc_symbols = fetch_swap_markets_raw("cmcbl")?.into_iter().map(|m| m.symbol).collect::>(); usdt_symbols.extend(usdc_symbols); Ok(usdt_symbols) } pub(super) fn fetch_inverse_swap_markets() -> Result> { let markets = fetch_swap_markets_raw("dmcbl")? .into_iter() .filter(|market| market.symbol.ends_with("_DMCBL")) .map(to_market) .collect::>(); Ok(markets) } pub(super) fn fetch_inverse_future_markets() -> Result> { let markets = fetch_swap_markets_raw("dmcbl")? .into_iter() .filter(|market| !market.symbol.ends_with("_DMCBL")) .map(to_market) .collect::>(); Ok(markets) } pub(super) fn fetch_linear_swap_markets() -> Result> { let markets = fetch_swap_markets_raw("umcbl")?.into_iter().map(to_market).collect::>(); Ok(markets) } fn to_market(m: SwapMarket) -> Market { let market_type = if m.symbol.ends_with("_UMCBL") { MarketType::LinearSwap } else if m.symbol.ends_with("_DMCBL") { MarketType::InverseSwap } else if m.symbol.contains("_UMCBL_") { MarketType::LinearFuture } else if m.symbol.contains("_DMCBL_") { MarketType::InverseFuture } else { panic!("unexpected symbol: {}", m.symbol); }; let delivery_time = if market_type == MarketType::InverseFuture || market_type == MarketType::LinearFuture { let date = m.symbol.split('_').last().unwrap(); debug_assert_eq!(date.len(), 6); // e.g., 230331 let year = &date[..2]; let month = &date[2..4]; let day = &date[4..]; let delivery_time = DateTime::parse_from_rfc3339(format!("20{year}-{month}-{day}T00:00:00+00:00").as_str()) .unwrap() .timestamp_millis() as u64; Some(delivery_time) } else { None }; Market { exchange: EXCHANGE_NAME.to_string(), market_type, symbol: m.symbol.clone(), base_id: m.baseCoin.clone(), quote_id: m.quoteCoin.clone(), settle_id: Some(m.supportMarginCoins[0].clone()), base: m.baseCoin.clone(), quote: m.quoteCoin.clone(), settle: Some(m.supportMarginCoins[0].clone()), active: true, margin: true, fees: Fees { maker: m.makerFeeRate.parse::().unwrap(), taker: m.takerFeeRate.parse::().unwrap(), }, precision: Precision { tick_size: 1.0 / (10_i64.pow(m.pricePlace.parse::().unwrap()) as f64), lot_size: 1.0 / (10_i64.pow(m.volumePlace.parse::().unwrap()) as f64), }, quantity_limit: Some(QuantityLimit { min: m.minTradeNum.parse::().ok(), max: None, notional_min: None, notional_max: None, }), contract_value: Some(1.0), // TODO: delivery_date: delivery_time, info: serde_json::to_value(&m).unwrap().as_object().unwrap().clone(), } } ================================================ FILE: crypto-markets/src/exchanges/bitget/mod.rs ================================================ pub(super) mod bitget_spot; pub(super) mod bitget_swap; use crate::{error::Result, Market, MarketType}; pub(super) const EXCHANGE_NAME: &str = "bitget"; pub(crate) fn fetch_symbols(market_type: MarketType) -> Result> { match market_type { MarketType::Spot => bitget_spot::fetch_spot_symbols(), MarketType::InverseSwap => bitget_swap::fetch_inverse_swap_symbols(), MarketType::LinearSwap => bitget_swap::fetch_linear_swap_symbols(), MarketType::InverseFuture => bitget_swap::fetch_inverse_future_symbols(), _ => panic!("Unsupported market_type: {market_type}"), } } pub(crate) fn fetch_markets(market_type: MarketType) -> Result> { match market_type { MarketType::Spot => bitget_spot::fetch_spot_markets(), MarketType::InverseSwap => bitget_swap::fetch_inverse_swap_markets(), MarketType::LinearSwap => bitget_swap::fetch_linear_swap_markets(), MarketType::InverseFuture => bitget_swap::fetch_inverse_future_markets(), _ => panic!("Unsupported market_type: {market_type}"), } } ================================================ FILE: crypto-markets/src/exchanges/bithumb.rs ================================================ use std::collections::HashMap; use super::utils::http_get; use crate::{ error::{Error, Result}, Fees, Market, MarketType, Precision, }; use serde::{Deserialize, Serialize}; use serde_json::Value; pub(crate) fn fetch_symbols(market_type: MarketType) -> Result> { match market_type { MarketType::Spot => fetch_spot_symbols(), _ => panic!("Unsupported market_type: {market_type}"), } } pub(crate) fn fetch_markets(market_type: MarketType) -> Result> { match market_type { MarketType::Spot => fetch_spot_markets(), _ => panic!("Unsupported market_type: {market_type}"), } } #[derive(Serialize, Deserialize)] #[allow(non_snake_case)] struct CoinConfig { makerFeeRate: serde_json::Value, // String or i64 minTxAmt: Option, name: String, depositStatus: String, fullName: String, takerFeeRate: serde_json::Value, // String or i64 minWithdraw: String, withdrawFee: String, withdrawStatus: String, #[serde(flatten)] extra: HashMap, } #[derive(Serialize, Deserialize)] #[allow(non_snake_case)] struct PercentPrice { multiplierDown: String, multiplierUp: String, } #[derive(Serialize, Deserialize)] #[allow(non_snake_case)] struct SpotConfig { symbol: String, percentPrice: PercentPrice, accuracy: Vec, openPrice: String, openTime: i64, #[serde(flatten)] extra: HashMap, } #[derive(Serialize, Deserialize)] #[allow(non_snake_case)] struct Data { coinConfig: Vec, spotConfig: Vec, } #[derive(Serialize, Deserialize)] struct Response { data: Data, code: String, msg: String, timestamp: i64, } // see https://github.com/bithumb-pro/bithumb.pro-official-api-docs/blob/master/rest-api.md#2-config-detail fn fetch_spot_coing() -> Result { let txt = http_get("https://global-openapi.bithumb.pro/openapi/v1/spot/config", None)?; let resp = serde_json::from_str::(&txt)?; if resp.code != "0" { Err(Error(txt)) } else { Ok(resp.data) } } fn fetch_spot_symbols() -> Result> { let symbols = fetch_spot_coing()?.spotConfig.into_iter().map(|m| m.symbol).collect::>(); Ok(symbols) } fn fetch_spot_markets() -> Result> { let markets = fetch_spot_coing()? .spotConfig .into_iter() .map(|m| { let info = serde_json::to_value(&m).unwrap().as_object().unwrap().clone(); let pair = crypto_pair::normalize_pair(&m.symbol, "bithumb").unwrap(); let (base, quote) = { let v: Vec<&str> = pair.split('/').collect(); (v[0].to_string(), v[1].to_string()) }; let (base_id, quote_id) = { let v: Vec<&str> = m.symbol.split('-').collect(); (v[0].to_string(), v[1].to_string()) }; Market { exchange: "bithumb".to_string(), market_type: MarketType::Spot, symbol: m.symbol, base_id, quote_id, settle_id: None, base, quote, settle: None, active: true, margin: false, // see https://www.bitglobal.com/en-us/fee fees: Fees { maker: 0.001, taker: 0.001 }, precision: Precision { tick_size: 1.0 / (10_i64.pow(m.accuracy[0].parse::().unwrap()) as f64), lot_size: 1.0 / (10_i64.pow(m.accuracy[1].parse::().unwrap()) as f64), }, quantity_limit: None, contract_value: None, delivery_date: None, info, } }) .collect::>(); Ok(markets) } ================================================ FILE: crypto-markets/src/exchanges/bitmex.rs ================================================ use std::collections::HashMap; use super::utils::http_get; use crate::{ error::Result, market::{Fees, Precision}, Market, MarketType, }; use chrono::DateTime; use crypto_pair::get_market_type; use serde::{Deserialize, Serialize}; use serde_json::Value; pub(crate) fn fetch_symbols(market_type: MarketType) -> Result> { let instruments = fetch_instruments(market_type)?; Ok(instruments.into_iter().map(|x| x.symbol).collect::>()) } pub(crate) fn fetch_markets(market_type: MarketType) -> Result> { let instruments = fetch_instruments(market_type)?; let markets: Vec = instruments .into_iter() .map(|x| { let info = serde_json::to_value(&x).unwrap().as_object().unwrap().clone(); let base_id = x.underlying; let quote_id = x.quoteCurrency; let pair = crypto_pair::normalize_pair(&x.symbol, "bitmex").unwrap(); let (base, quote) = { let v: Vec<&str> = pair.split('/').collect(); (v[0].to_string(), v[1].to_string()) }; let market_type = if market_type == MarketType::Unknown { get_market_type(&x.symbol, "bitmex", None) } else { market_type }; Market { exchange: "bitmex".to_string(), market_type, symbol: x.symbol, base_id, quote_id, settle_id: Some(x.settlCurrency.clone()), base, quote, settle: Some(crypto_pair::normalize_currency(x.settlCurrency.as_str(), "bitmex")), active: x.state == "Open", margin: true, fees: Fees { maker: x.makerFee, taker: x.takerFee }, precision: Precision { tick_size: x.tickSize, lot_size: x.lotSize }, quantity_limit: None, contract_value: if market_type != MarketType::Spot { if let Some(y) = x.underlyingToSettleMultiplier { Some(x.multiplier / y) } else { Some(x.multiplier / x.quoteToSettleMultiplier.unwrap()) } } else { None }, delivery_date: if let Some(expiry) = x.expiry { let timestamp = DateTime::parse_from_rfc3339(&expiry).unwrap(); Some(timestamp.timestamp_millis() as u64) } else { None }, info, } }) .collect(); Ok(markets) } // https://bitmex.freshdesk.com/en/support/solutions/articles/13000081130-instrument #[derive(Clone, Serialize, Deserialize)] #[allow(non_snake_case)] struct Instrument { symbol: String, // The contract for this position. rootSymbol: String, // Root symbol for the instrument, used for grouping on the frontend. state: String, /* State of the instrument, it can be * `Open`Closed`Unlisted`Expired`Cleared. */ typ: String, // Type of the instrument (e.g. Futures, Perpetual Contracts). listing: String, front: Option, expiry: Option, settle: Option, listedSettle: Option, inverseLeg: Option, positionCurrency: String, /* Currency for position of this contract. If not null, 1 contract * = 1 positionCurrency. */ underlying: String, // Defines the underlying asset of the instrument (e.g.XBT). quoteCurrency: String, // Currency of the quote price. underlyingSymbol: String, // Symbol of the underlying asset. reference: String, // Venue of the reference symbol. referenceSymbol: String, // Symbol of index being referenced (e.g. .BXBT). calcInterval: Option, publishInterval: Option, publishTime: Option, maxOrderQty: i64, maxPrice: f64, lotSize: f64, tickSize: f64, multiplier: f64, settlCurrency: String, underlyingToPositionMultiplier: Option, underlyingToSettleMultiplier: Option, quoteToSettleMultiplier: Option, isQuanto: bool, isInverse: bool, initMargin: f64, maintMargin: f64, riskLimit: Option, riskStep: Option, limit: Option, capped: bool, taxed: bool, deleverage: bool, makerFee: f64, takerFee: f64, settlementFee: f64, insuranceFee: f64, fundingBaseSymbol: String, fundingQuoteSymbol: String, fundingPremiumSymbol: String, fundingTimestamp: Option, fundingInterval: Option, fundingRate: Option, indicativeFundingRate: Option, rebalanceTimestamp: Option, rebalanceInterval: Option, openingTimestamp: String, closingTimestamp: String, sessionInterval: String, prevTotalVolume: i64, totalVolume: i64, volume: i64, volume24h: i64, prevTotalTurnover: i64, totalTurnover: i64, turnover: i64, turnover24h: i64, homeNotional24h: f64, foreignNotional24h: f64, lastTickDirection: String, hasLiquidity: bool, openInterest: i64, openValue: i64, fairMethod: String, markMethod: String, timestamp: String, #[serde(flatten)] extra: HashMap, } fn fetch_instruments(market_type: MarketType) -> Result> { let text = http_get("https://www.bitmex.com/api/v1/instrument/active", None)?; let instruments: Vec = serde_json::from_str::>(&text)? .into_iter() .filter(|x| x.state == "Open" && x.hasLiquidity && x.volume24h > 0 && x.turnover24h > 0) .collect(); let spot: Vec = instruments.iter().filter(|x| x.typ == "IFXXXP").cloned().collect(); let swap: Vec = instruments.iter().filter(|x| x.typ == "FFWCSX").cloned().collect(); let futures: Vec = instruments.iter().filter(|x| x.typ == "FFCCSX").cloned().collect(); // let fx: Vec = instruments // .iter() // .filter(|x| x.typ == "FFWCSF") // .cloned() // .collect(); for x in swap.iter() { assert_eq!("FundingRate", x.fairMethod.as_str()); assert!(x.expiry.is_none()); assert!(x.symbol[x.symbol.len() - 1..].parse::().is_err()); if let Some(pos) = x.symbol.rfind('_') { // e.g., ETHUSD_ETH assert_eq!(&(x.symbol[..pos]), format!("{}{}", x.underlying, x.quoteCurrency)); } else { assert_eq!(x.symbol, format!("{}{}", x.underlying, x.quoteCurrency)); } // println!("{}, {}, {}, {}, {}, {}", x.symbol, x.rootSymbol, // x.quoteCurrency, x.settlCurrency, x.positionCurrency, x.underlying); } for x in futures.iter() { assert_eq!("ImpactMidPrice", x.fairMethod.as_str()); assert!(x.expiry.is_some()); if let Some(pos) = x.symbol.rfind('_') { // e.g., ETHUSDM22_ETH assert!(x.symbol[pos - 2..pos].parse::().is_ok()); } else { assert!(x.symbol[x.symbol.len() - 2..].parse::().is_ok()); } } // Inverse for x in instruments.iter().filter(|x| x.isInverse) { assert!(x.multiplier < 0.0); assert_eq!(x.quoteCurrency, x.positionCurrency); } // Quanto for x in instruments.iter().filter(|x| x.isQuanto) { assert!(x.positionCurrency.is_empty()); // settled in XBT, quoted in USD or USDC assert_eq!(x.settlCurrency.to_uppercase(), "XBT"); if x.typ != "FFWCSF" { assert!(x.quoteCurrency == "USD" || x.quoteCurrency == "USDC"); } } for x in instruments.iter().filter(|x| x.positionCurrency.is_empty() && x.typ != "IFXXXP") { assert!(x.isQuanto); } // Linear for x in instruments.iter().filter(|x| !x.isQuanto && !x.isInverse && x.typ != "IFXXXP") { // settled in XBT, qouted in XBT // or settled in USDT, qouted in USDT assert_eq!(x.settlCurrency.to_uppercase(), x.quoteCurrency); } let filtered: Vec = match market_type { MarketType::Unknown => instruments, MarketType::Spot => spot, MarketType::LinearSwap => { swap.iter().filter(|x| !x.isQuanto && !x.isInverse).cloned().collect() } MarketType::InverseSwap => { swap.iter().filter(|x| !x.isQuanto && x.isInverse).cloned().collect() } MarketType::QuantoSwap => swap.iter().filter(|x| x.isQuanto).cloned().collect(), MarketType::LinearFuture => { futures.iter().filter(|x| !x.isInverse && !x.isQuanto).cloned().collect() } MarketType::InverseFuture => futures.iter().filter(|x| x.isInverse).cloned().collect(), MarketType::QuantoFuture => futures.iter().filter(|x| x.isQuanto).cloned().collect(), _ => panic!("Unsupported market_type: {market_type}"), }; Ok(filtered) } ================================================ FILE: crypto-markets/src/exchanges/bitstamp.rs ================================================ use super::utils::http_get; use crate::{error::Result, Fees, Market, MarketType, Precision}; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; pub(crate) fn fetch_symbols(market_type: MarketType) -> Result> { match market_type { MarketType::Spot => fetch_spot_symbols(), _ => panic!("Unsupported market_type: {market_type}"), } } pub(crate) fn fetch_markets(market_type: MarketType) -> Result> { match market_type { MarketType::Spot => fetch_spot_markets(), _ => panic!("Unsupported market_type: {market_type}"), } } #[derive(Serialize, Deserialize)] struct SpotMarket { base_decimals: i64, minimum_order: String, name: String, counter_decimals: i64, trading: String, url_symbol: String, description: String, #[serde(flatten)] extra: HashMap, } // see fn fetch_spot_markets_raw() -> Result> { let txt = http_get("https://www.bitstamp.net/api/v2/trading-pairs-info/", None)?; let markets = serde_json::from_str::>(&txt)?; Ok(markets.into_iter().filter(|m| m.trading == "Enabled").collect()) } fn fetch_spot_symbols() -> Result> { let symbols = fetch_spot_markets_raw()?.into_iter().map(|m| m.url_symbol).collect::>(); Ok(symbols) } fn fetch_spot_markets() -> Result> { let markets = fetch_spot_markets_raw()? .into_iter() .map(|m| { let info = serde_json::to_value(&m).unwrap().as_object().unwrap().clone(); let pair = crypto_pair::normalize_pair(&m.url_symbol, "bitstamp").unwrap(); let (base, quote) = { let v: Vec<&str> = pair.split('/').collect(); (v[0].to_string(), v[1].to_string()) }; let (base_id, quote_id) = { let v: Vec<&str> = m.name.split('/').collect(); (v[0].to_string(), v[1].to_string()) }; Market { exchange: "bitstamp".to_string(), market_type: MarketType::Spot, symbol: m.url_symbol, base_id, quote_id, settle_id: None, base, quote, settle: None, active: true, margin: true, // see https://www.bitstamp.net/fee-schedule/ fees: Fees { maker: 0.005, taker: 0.005 }, precision: Precision { tick_size: 1.0 / (10_i64.pow(m.base_decimals as u32) as f64), lot_size: 1.0 / (10_i64.pow(m.counter_decimals as u32) as f64), }, quantity_limit: None, contract_value: None, delivery_date: None, info, } }) .collect::>(); Ok(markets) } ================================================ FILE: crypto-markets/src/exchanges/bitz/bitz_spot.rs ================================================ use std::collections::HashMap; use super::super::utils::http_get; use crate::error::{Error, Result}; use serde::{Deserialize, Serialize}; use serde_json::Value; #[derive(Clone, Serialize, Deserialize)] #[allow(non_snake_case)] struct SpotMarket { id: String, symbol: String, baseCurrency: String, quoteCurrency: String, amountPrecision: String, pricePrecision: String, status: String, minOrderAmt: String, maxOrderAmt: String, buyFree: String, sellFree: String, marketBuyFloat: String, #[serde(flatten)] extra: HashMap, } #[derive(Serialize, Deserialize)] struct Response { status: i64, msg: String, data: HashMap, time: i64, microtime: String, source: String, } // See https://apidocv2.bitz.plus/en/#get-data-of-trading-pairs fn fetch_spot_markets_raw() -> Result> { let txt = http_get("https://apiv2.bitz.com/V2/Market/symbolList", None)?; let resp = serde_json::from_str::(&txt)?; if resp.status != 200 { Err(Error(txt)) } else { let markets = resp.data.values().cloned().filter(|x| x.status == "1").collect::>(); Ok(markets) } } pub(super) fn fetch_spot_symbols() -> Result> { let markets = fetch_spot_markets_raw()?; let symbols: Vec = markets.into_iter().map(|m| m.symbol).collect(); Ok(symbols) } ================================================ FILE: crypto-markets/src/exchanges/bitz/bitz_swap.rs ================================================ use std::collections::HashMap; use super::super::utils::http_get; use crate::error::{Error, Result}; use serde::{Deserialize, Serialize}; use serde_json::Value; #[derive(Clone, Serialize, Deserialize)] #[allow(non_snake_case)] struct SwapMarket { contractId: String, // contract id symbol: String, // symbol settleAnchor: String, // settle anchor quoteAnchor: String, // quote anchor contractAnchor: String, // contract anchor contractValue: String, // contract Value pair: String, //contract market expiry: String, //delivery time (non-perpetual contract) maxLeverage: String, // max leverage maintanceMargin: String, //maintenance margin makerFee: String, // maker fee rate takerFee: String, // taker fee rate settleFee: String, // settlement fee rate priceDec: String, // floating point decimal of price anchorDec: String, // floating point decimal of quote anchor status: String, // status,1: trading, 0:pending,-1:permanent stop isreverse: String, // 1:reverse contract,-1: forward contract allowCross: String, // Allow cross position,1:Yes,-1:No allowLeverages: String, // Leverage multiple allowed by the system maxOrderNum: String, // max number of unfilled orders maxAmount: String, // max amount of a single order minAmount: String, // min amount of a single order maxPositionAmount: String, //max position amount #[serde(flatten)] extra: HashMap, } #[derive(Serialize, Deserialize)] struct Response { status: i64, msg: String, data: Vec, time: i64, microtime: String, source: String, } // See https://apidocv2.bitz.plus/en/#get-market-list-of-contract-transactions fn fetch_swap_markets_raw() -> Result> { let txt = http_get("https://apiv2.bitz.com/V2/Market/getContractCoin", None)?; let resp = serde_json::from_str::(&txt)?; if resp.status != 200 { Err(Error(txt)) } else { let markets: Vec = resp.data.into_iter().filter(|x| x.status == "1").collect(); Ok(markets) } } pub(super) fn fetch_inverse_swap_symbols() -> Result> { let symbols = fetch_swap_markets_raw()? .into_iter() .filter(|m| m.isreverse == "1") .map(|m| m.pair) .collect::>(); Ok(symbols) } pub(super) fn fetch_linear_swap_symbols() -> Result> { let symbols = fetch_swap_markets_raw()? .into_iter() .filter(|m| m.isreverse == "-1" && m.settleAnchor == "USDT") .map(|m| m.pair) .collect::>(); Ok(symbols) } ================================================ FILE: crypto-markets/src/exchanges/bitz/mod.rs ================================================ mod bitz_spot; mod bitz_swap; use crate::{error::Result, Market, MarketType}; pub(crate) fn fetch_symbols(market_type: MarketType) -> Result> { match market_type { MarketType::Spot => bitz_spot::fetch_spot_symbols(), MarketType::InverseSwap => bitz_swap::fetch_inverse_swap_symbols(), MarketType::LinearSwap => bitz_swap::fetch_linear_swap_symbols(), _ => panic!("Unsupported market_type: {market_type}"), } } pub(crate) fn fetch_markets(_market_type: MarketType) -> Result> { Ok(Vec::new()) } ================================================ FILE: crypto-markets/src/exchanges/bybit.rs ================================================ use std::collections::HashMap; use super::utils::http_get; use crate::{error::Result, Fees, Market, MarketType, Precision, QuantityLimit}; use chrono::{prelude::*, DateTime}; use serde::{Deserialize, Serialize}; use serde_json::Value; pub(crate) fn fetch_symbols(market_type: MarketType) -> Result> { match market_type { MarketType::InverseSwap => fetch_inverse_swap_symbols(), MarketType::LinearSwap => fetch_linear_swap_symbols(), MarketType::InverseFuture => fetch_inverse_future_symbols(), _ => panic!("Unsupported market_type: {market_type}"), } } pub(crate) fn fetch_markets(market_type: MarketType) -> Result> { match market_type { MarketType::InverseSwap => fetch_inverse_swap_markets(), MarketType::LinearSwap => fetch_linear_swap_markets(), MarketType::InverseFuture => fetch_inverse_future_markets(), _ => panic!("Unsupported market_type: {market_type}"), } } #[derive(Serialize, Deserialize)] struct LeverageFilter { min_leverage: i64, max_leverage: i64, leverage_step: String, } #[derive(Serialize, Deserialize)] struct PriceFilter { min_price: String, max_price: String, tick_size: String, } #[derive(Serialize, Deserialize)] struct LotSizeFilter { max_trading_qty: f64, min_trading_qty: f64, qty_step: f64, } #[derive(Serialize, Deserialize)] struct BybitMarket { name: String, alias: String, status: String, base_currency: String, quote_currency: String, price_scale: i64, taker_fee: String, maker_fee: String, leverage_filter: LeverageFilter, price_filter: PriceFilter, lot_size_filter: LotSizeFilter, #[serde(flatten)] extra: HashMap, } #[derive(Serialize, Deserialize)] struct Response { ret_code: i64, ret_msg: String, ext_code: String, ext_info: String, result: Vec, } // See https://bybit-exchange.github.io/docs/inverse/#t-querysymbol fn fetch_markets_raw() -> Result> { let txt = http_get("https://api.bybit.com/v2/public/symbols", None)?; let resp = serde_json::from_str::(&txt)?; assert_eq!(resp.ret_code, 0); Ok(resp.result.into_iter().filter(|m| m.status == "Trading").collect()) } fn fetch_inverse_swap_symbols() -> Result> { let symbols = fetch_markets_raw()? .into_iter() .filter(|m| m.name == m.alias && m.quote_currency == "USD") .map(|m| m.name) .collect::>(); Ok(symbols) } fn fetch_linear_swap_symbols() -> Result> { let symbols = fetch_markets_raw()? .into_iter() .filter(|m| m.name == m.alias && m.quote_currency == "USDT") .map(|m| m.name) .collect::>(); Ok(symbols) } fn fetch_inverse_future_symbols() -> Result> { let symbols = fetch_markets_raw()? .into_iter() .filter(|m| { m.quote_currency == "USD" && m.name[(m.name.len() - 2)..].parse::().is_ok() }) .map(|m| m.name) .collect::>(); Ok(symbols) } fn to_market(raw_market: &BybitMarket) -> Market { let pair = crypto_pair::normalize_pair(&raw_market.name, "bybit").unwrap(); let (base, quote) = { let v: Vec<&str> = pair.split('/').collect(); (v[0].to_string(), v[1].to_string()) }; let delivery_date: Option = if raw_market.name[(raw_market.name.len() - 2)..].parse::().is_ok() { let n = raw_market.alias.len(); let s = raw_market.alias.as_str(); let month = &s[(n - 4)..(n - 2)]; let day = &s[(n - 2)..]; let now = Utc::now(); let year = Utc::now().year(); let delivery_time = DateTime::parse_from_rfc3339( format!("{year}-{month}-{day}T00:00:00+00:00").as_str(), ) .unwrap(); let delivery_time = if delivery_time > now { delivery_time } else { DateTime::parse_from_rfc3339( format!("{}-{}-{}T00:00:00+00:00", year + 1, month, day).as_str(), ) .unwrap() }; assert!(delivery_time > now); Some(delivery_time.timestamp_millis() as u64) } else { None }; Market { exchange: "bybit".to_string(), market_type: if raw_market.name != raw_market.alias { MarketType::InverseFuture } else if raw_market.quote_currency == "USDT" { MarketType::LinearSwap } else { MarketType::InverseSwap }, symbol: raw_market.name.to_string(), base_id: raw_market.base_currency.to_string(), quote_id: raw_market.quote_currency.to_string(), settle_id: if raw_market.quote_currency == "USDT" { Some(raw_market.quote_currency.to_string()) } else { Some(raw_market.base_currency.to_string()) }, base, quote, settle: if raw_market.quote_currency == "USDT" { Some(raw_market.quote_currency.to_string()) } else { Some(raw_market.base_currency.to_string()) }, active: raw_market.status == "Trading", margin: true, fees: Fees { maker: raw_market.maker_fee.parse::().unwrap(), taker: raw_market.taker_fee.parse::().unwrap(), }, precision: Precision { tick_size: raw_market.price_filter.tick_size.parse::().unwrap(), lot_size: raw_market.lot_size_filter.qty_step, }, quantity_limit: Some(QuantityLimit { min: Some(raw_market.lot_size_filter.min_trading_qty), max: Some(raw_market.lot_size_filter.max_trading_qty), notional_min: None, notional_max: None, }), contract_value: Some(1.0), delivery_date, info: serde_json::to_value(raw_market).unwrap().as_object().unwrap().clone(), } } fn fetch_inverse_swap_markets() -> Result> { let markets = fetch_markets_raw()? .into_iter() .filter(|m| m.name == m.alias && m.quote_currency == "USD") .map(|m| to_market(&m)) .collect::>(); Ok(markets) } fn fetch_linear_swap_markets() -> Result> { let markets = fetch_markets_raw()? .into_iter() .filter(|m| m.name == m.alias && m.quote_currency == "USDT") .map(|m| to_market(&m)) .collect::>(); Ok(markets) } fn fetch_inverse_future_markets() -> Result> { let markets = fetch_markets_raw()? .into_iter() .filter(|m| { m.quote_currency == "USD" && m.name[(m.name.len() - 2)..].parse::().is_ok() }) .map(|m| to_market(&m)) .collect::>(); Ok(markets) } ================================================ FILE: crypto-markets/src/exchanges/coinbase_pro.rs ================================================ use std::collections::HashMap; use super::utils::http_get; use crate::{error::Result, Fees, Market, MarketType, Precision, QuantityLimit}; use serde::{Deserialize, Serialize}; use serde_json::Value; pub(crate) fn fetch_symbols(market_type: MarketType) -> Result> { match market_type { MarketType::Spot => fetch_spot_symbols(), _ => panic!("Unsupported market_type: {market_type}"), } } pub(crate) fn fetch_markets(market_type: MarketType) -> Result> { match market_type { MarketType::Spot => fetch_spot_markets(), _ => panic!("Unsupported market_type: {market_type}"), } } #[derive(Serialize, Deserialize)] struct SpotMarket { id: String, base_currency: String, quote_currency: String, quote_increment: String, base_increment: String, display_name: String, min_market_funds: Option, max_market_funds: Option, margin_enabled: bool, post_only: bool, limit_only: bool, cancel_only: bool, trading_disabled: bool, status: String, status_message: String, #[serde(flatten)] extra: HashMap, } // see fn fetch_spot_markets_raw() -> Result> { let txt = http_get("https://api.exchange.coinbase.com/products", None)?; let markets = serde_json::from_str::>(&txt)?; Ok(markets) } fn fetch_spot_symbols() -> Result> { let symbols = fetch_spot_markets_raw()? .into_iter() .filter(|m| !m.trading_disabled && m.status == "online" && !m.cancel_only) .map(|m| m.id) .collect::>(); Ok(symbols) } fn fetch_spot_markets() -> Result> { let markets = fetch_spot_markets_raw()? .into_iter() .map(|m| { let info = serde_json::to_value(&m).unwrap().as_object().unwrap().clone(); let pair = crypto_pair::normalize_pair(&m.id, "coinbase_pro").unwrap(); let (base, quote) = { let v: Vec<&str> = pair.split('/').collect(); (v[0].to_string(), v[1].to_string()) }; Market { exchange: "coinbase_pro".to_string(), market_type: MarketType::Spot, symbol: m.id, base_id: m.base_currency, quote_id: m.quote_currency, settle_id: None, base, quote, settle: None, active: !m.trading_disabled && m.status == "online" && !m.cancel_only, margin: m.margin_enabled, // // see https://pro.coinbase.com/fees, https://pro.coinbase.com/orders/fees fees: Fees { maker: 0.005, taker: 0.005 }, precision: Precision { tick_size: m.quote_increment.parse::().unwrap(), lot_size: m.base_increment.parse::().unwrap(), }, quantity_limit: Some(QuantityLimit { min: None, max: None, notional_min: m.min_market_funds.map(|x| x.parse::().unwrap()), notional_max: m.max_market_funds.map(|x| x.parse::().unwrap()), }), contract_value: None, delivery_date: None, info, } }) .collect::>(); Ok(markets) } ================================================ FILE: crypto-markets/src/exchanges/deribit/mod.rs ================================================ mod utils; use crate::{error::Result, Market, MarketType}; pub(crate) fn fetch_symbols(market_type: MarketType) -> Result> { match market_type { MarketType::InverseFuture => utils::fetch_inverse_future_symbols(), MarketType::InverseSwap => utils::fetch_inverse_swap_symbols(), MarketType::EuropeanOption => utils::fetch_option_symbols(), _ => panic!("Unsupported market_type: {market_type}"), } } pub(crate) fn fetch_markets(market_type: MarketType) -> Result> { match market_type { MarketType::InverseFuture => utils::fetch_inverse_future_markets(), MarketType::InverseSwap => utils::fetch_inverse_swap_markets(), MarketType::EuropeanOption => utils::fetch_option_markets(), _ => panic!("Unsupported market_type: {market_type}"), } } ================================================ FILE: crypto-markets/src/exchanges/deribit/utils.rs ================================================ use super::super::utils::http_get; use crate::{ error::{Error, Result}, market::{Fees, Precision, QuantityLimit}, Market, }; use crypto_market_type::MarketType; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; fn check_error_in_body(body: String) -> Result { let obj = serde_json::from_str::>(&body).unwrap(); if obj.contains_key("error") { Err(Error(body)) } else { Ok(body) } } pub(super) fn deribit_http_get(url: &str) -> Result { let ret = http_get(url, None); match ret { Ok(body) => check_error_in_body(body), Err(_) => ret, } } #[derive(Serialize, Deserialize)] #[allow(non_snake_case)] struct DeribitResponse { id: Option, jsonrpc: String, result: Vec, usIn: i64, usOut: i64, usDiff: i64, testnet: bool, #[serde(flatten)] extra: HashMap, } #[derive(Serialize, Deserialize)] struct Instrument { tick_size: f64, taker_commission: f64, strike: Option, settlement_period: String, quote_currency: String, min_trade_amount: f64, max_liquidation_commission: Option, max_leverage: Option, maker_commission: f64, kind: String, is_active: bool, instrument_name: String, expiration_timestamp: u64, creation_timestamp: u64, contract_size: f64, // TODO: why i64 panic ? block_trade_commission: f64, base_currency: String, #[serde(flatten)] extra: HashMap, } /// Get active trading instruments. /// /// doc: /// /// `currency`, available values are `BTC` and `ETH`. /// /// `kind`, available values are `future` and `option`. /// /// Example: fn fetch_instruments(currency: &str, kind: &str) -> Result> { let url = format!( "https://www.deribit.com/api/v2/public/get_instruments?currency={currency}&kind={kind}" ); let txt = deribit_http_get(&url)?; let resp = serde_json::from_str::>(&txt)?; Ok(resp.result) } fn fetch_raw_markets(kind: &str) -> Result> { let mut all_markets: Vec = Vec::new(); let result = fetch_instruments("BTC", kind); match result { Ok(mut instruments) => { all_markets.append(&mut instruments); } Err(error) => { return Err(error); } } let result = fetch_instruments("ETH", kind); match result { Ok(mut instruments) => { all_markets.append(&mut instruments); } Err(error) => { return Err(error); } } Ok(all_markets.into_iter().filter(|x| x.is_active).collect()) } fn fetch_symbols(kind: &str) -> Result> { let all_markets = fetch_raw_markets(kind)?; let all_symbols: Vec = all_markets.into_iter().map(|x| x.instrument_name).collect(); Ok(all_symbols) } pub(super) fn fetch_inverse_future_symbols() -> Result> { let result = fetch_symbols("future"); match result { Ok(symbols) => Ok(symbols.into_iter().filter(|x| !x.ends_with("-PERPETUAL")).collect()), Err(error) => Err(error), } } pub(super) fn fetch_inverse_swap_symbols() -> Result> { let result = fetch_symbols("future"); match result { Ok(symbols) => Ok(symbols.into_iter().filter(|x| x.ends_with("-PERPETUAL")).collect()), Err(error) => Err(error), } } pub(super) fn fetch_option_symbols() -> Result> { fetch_symbols("option") } fn to_market(raw_market: &Instrument) -> Market { let market_type = if raw_market.kind == "future" { if raw_market.instrument_name.ends_with("-PERPETUAL") { MarketType::InverseSwap } else { MarketType::InverseFuture } } else { MarketType::EuropeanOption }; let pair = crypto_pair::normalize_pair(&raw_market.instrument_name, "deribit").unwrap(); let (base, quote) = { let v: Vec<&str> = pair.split('/').collect(); (v[0].to_string(), v[1].to_string()) }; Market { exchange: "deribit".to_string(), market_type, symbol: raw_market.instrument_name.to_string(), base_id: raw_market.base_currency.to_string(), quote_id: raw_market.quote_currency.to_string(), settle_id: Some(raw_market.base_currency.to_string()), base: base.clone(), quote, settle: Some(base), active: raw_market.is_active, margin: true, fees: Fees { maker: raw_market.maker_commission, taker: raw_market.taker_commission }, precision: Precision { tick_size: raw_market.tick_size, lot_size: raw_market.min_trade_amount, }, quantity_limit: Some(QuantityLimit { min: Some(raw_market.min_trade_amount), max: None, notional_min: None, notional_max: None, }), contract_value: Some(raw_market.contract_size), delivery_date: if market_type == MarketType::InverseSwap { None } else { Some(raw_market.expiration_timestamp) }, info: serde_json::to_value(raw_market).unwrap().as_object().unwrap().clone(), } } pub(super) fn fetch_inverse_future_markets() -> Result> { let raw_markets = fetch_raw_markets("future")?; let markets: Vec = raw_markets .into_iter() .filter(|x| !x.instrument_name.ends_with("-PERPETUAL")) .map(|x| to_market(&x)) .collect(); Ok(markets) } pub(super) fn fetch_inverse_swap_markets() -> Result> { let raw_markets = fetch_raw_markets("future")?; let markets: Vec = raw_markets .into_iter() .filter(|x| x.instrument_name.ends_with("-PERPETUAL")) .map(|x| to_market(&x)) .collect(); Ok(markets) } pub(super) fn fetch_option_markets() -> Result> { let raw_markets = fetch_raw_markets("option")?; let markets: Vec = raw_markets.into_iter().map(|x| to_market(&x)).collect(); Ok(markets) } ================================================ FILE: crypto-markets/src/exchanges/dydx/dydx_swap.rs ================================================ use std::collections::HashMap; use super::super::utils::http_get; use crate::{error::Result, Fees, Market, Precision, QuantityLimit}; use crypto_market_type::MarketType; use serde::{Deserialize, Serialize}; use serde_json::Value; const BASE_URL: &str = "https://api.dydx.exchange"; #[derive(Clone, Serialize, Deserialize)] #[allow(non_snake_case)] struct PerpetualMarket { market: String, status: String, baseAsset: String, quoteAsset: String, stepSize: String, tickSize: String, minOrderSize: String, #[serde(rename = "type")] type_: String, #[serde(flatten)] extra: HashMap, } #[derive(Clone, Serialize, Deserialize)] #[allow(non_snake_case)] struct MarketsResponse { markets: HashMap, } // See https://docs.dydx.exchange/#get-markets fn fetch_markets_raw() -> Result> { let txt = http_get(format!("{BASE_URL}/v3/markets").as_str(), None)?; let resp = serde_json::from_str::(&txt)?; Ok(resp .markets .values() .cloned() .filter(|x| x.status == "ONLINE") .collect::>()) } pub(super) fn fetch_linear_swap_symbols() -> Result> { let markets = fetch_markets_raw()?; let symbols = markets.into_iter().map(|m| m.market).collect::>(); Ok(symbols) } pub(super) fn fetch_linear_swap_markets() -> Result> { let markets = fetch_markets_raw()? .into_iter() .map(|m| { let info = serde_json::to_value(&m).unwrap().as_object().unwrap().clone(); let pair = crypto_pair::normalize_pair(&m.market, "dydx").unwrap(); let (base, quote) = { let v: Vec<&str> = pair.split('/').collect(); (v[0].to_string(), v[1].to_string()) }; Market { exchange: "dydx".to_string(), market_type: MarketType::LinearSwap, symbol: m.market, base_id: m.baseAsset, quote_id: m.quoteAsset, settle_id: Some(quote.clone()), base, quote: quote.clone(), settle: Some(quote), active: m.status == "ONLINE", margin: true, // see https://trade.dydx.exchange/portfolio/fees fees: Fees { maker: 0.0005, taker: 0.0001 }, precision: Precision { tick_size: m.tickSize.parse::().unwrap(), lot_size: m.stepSize.parse::().unwrap(), }, quantity_limit: Some(QuantityLimit { min: m.minOrderSize.parse::().ok(), max: None, notional_min: None, notional_max: None, }), contract_value: Some(1.0), delivery_date: None, info, } }) .collect::>(); Ok(markets) } ================================================ FILE: crypto-markets/src/exchanges/dydx/mod.rs ================================================ mod dydx_swap; use crate::{error::Result, Market, MarketType}; pub(crate) fn fetch_symbols(market_type: MarketType) -> Result> { match market_type { MarketType::LinearSwap => dydx_swap::fetch_linear_swap_symbols(), _ => panic!("dydX does NOT have the {market_type} market"), } } pub(crate) fn fetch_markets(market_type: MarketType) -> Result> { match market_type { MarketType::LinearSwap => dydx_swap::fetch_linear_swap_markets(), _ => panic!("dydX does NOT have the {market_type} market"), } } ================================================ FILE: crypto-markets/src/exchanges/ftx.rs ================================================ use std::collections::HashMap; use super::utils::http_get; use crate::{error::Result, Fees, Market, MarketType, Precision}; use chrono::{prelude::*, DateTime}; use serde::{Deserialize, Serialize}; use serde_json::Value; pub(crate) fn fetch_symbols(market_type: MarketType) -> Result> { match market_type { MarketType::Spot => fetch_spot_symbols(), MarketType::LinearSwap => fetch_linear_swap_symbols(), MarketType::LinearFuture => fetch_linear_future_symbols(), MarketType::Move => fetch_move_symbols(), MarketType::BVOL => fetch_bvol_symbols(), _ => panic!("Unsupported market_type: {market_type}"), } } pub(crate) fn fetch_markets(market_type: MarketType) -> Result> { match market_type { MarketType::Spot => fetch_spot_markets(), MarketType::LinearSwap => fetch_linear_swap_markets(), MarketType::LinearFuture => fetch_linear_future_markets(), MarketType::Move => fetch_move_markets(), MarketType::BVOL => fetch_bvol_markets(), _ => panic!("Unsupported market_type: {market_type}"), } } #[derive(Serialize, Deserialize)] #[allow(non_snake_case)] struct FtxMarket { name: String, baseCurrency: Option, quoteCurrency: Option, #[serde(rename = "type")] type_: String, underlying: Option, enabled: bool, postOnly: bool, priceIncrement: f64, sizeIncrement: f64, restricted: bool, #[serde(flatten)] extra: HashMap, } #[derive(Serialize, Deserialize)] struct Response { success: bool, result: Vec, } fn fetch_markets_raw() -> Result> { let txt = http_get("https://ftx.com/api/markets", None)?; let resp = serde_json::from_str::(&txt)?; assert!(resp.success); let valid: Vec = resp.result.into_iter().filter(|x| x.enabled).collect(); Ok(valid) } fn fetch_spot_symbols() -> Result> { let markets = fetch_markets_raw()?; let symbols: Vec = markets .into_iter() .filter(|x| x.type_ == "spot" && !x.name.contains("BVOL/")) .map(|x| x.name) .collect(); Ok(symbols) } fn fetch_linear_swap_symbols() -> Result> { let markets = fetch_markets_raw()?; let symbols: Vec = markets .into_iter() .filter(|x| x.type_ == "future" && x.name.ends_with("-PERP")) .map(|x| x.name) .collect(); Ok(symbols) } fn fetch_linear_future_symbols() -> Result> { let markets = fetch_markets_raw()?; let symbols: Vec = markets .into_iter() .filter(|x| { x.type_ == "future" && !x.name.ends_with("-PERP") && !x.name.contains("-MOVE-") && x.name[(x.name.len() - 4)..].parse::().is_ok() && x.name.contains('-') }) .map(|x| x.name) .collect(); Ok(symbols) } fn fetch_move_symbols() -> Result> { let markets = fetch_markets_raw()?; let symbols: Vec = markets .into_iter() .filter(|x| x.type_ == "future" && x.name.contains("-MOVE-")) .map(|x| x.name) .collect(); Ok(symbols) } fn fetch_bvol_symbols() -> Result> { let markets = fetch_markets_raw()?; let symbols: Vec = markets .into_iter() .filter(|x| x.type_ == "spot" && x.name.contains("BVOL/")) .map(|x| x.name) .collect(); Ok(symbols) } fn to_market(raw_market: &FtxMarket) -> Market { let pair = crypto_pair::normalize_pair(&raw_market.name, "ftx").unwrap(); let (base, quote) = { let v: Vec<&str> = pair.split('/').collect(); (v[0].to_string(), v[1].to_string()) }; let market_type = if raw_market.type_ == "spot" { if raw_market.name.contains("BVOL/") { MarketType::BVOL } else { MarketType::Spot } } else if raw_market.type_ == "future" { if raw_market.name.ends_with("-PERP") { MarketType::LinearSwap } else if raw_market.name.contains("-MOVE-") { MarketType::Move } else { MarketType::LinearFuture } } else { panic!("Unsupported type: {}", raw_market.type_); }; let delivery_date: Option = if raw_market.name[(raw_market.name.len() - 4)..].parse::().is_ok() { let n = raw_market.name.len(); let s = raw_market.name.as_str(); let month = &s[(n - 4)..(n - 2)]; let day = &s[(n - 2)..]; let now = Utc::now(); let year = Utc::now().year(); let delivery_time = DateTime::parse_from_rfc3339( format!("{year}-{month}-{day}T00:00:00+00:00").as_str(), ) .unwrap(); let delivery_time = if delivery_time > now { delivery_time } else { DateTime::parse_from_rfc3339( format!("{}-{}-{}T00:00:00+00:00", year + 1, month, day).as_str(), ) .unwrap() }; assert!(delivery_time > now); Some(delivery_time.timestamp_millis() as u64) } else { None }; Market { exchange: "ftx".to_string(), market_type, symbol: raw_market.name.to_string(), base_id: if raw_market.type_ == "spot" { raw_market.baseCurrency.clone().unwrap() } else { raw_market.underlying.clone().unwrap() }, quote_id: if raw_market.type_ == "spot" { raw_market.quoteCurrency.clone().unwrap() } else { "USD".to_string() }, settle_id: if raw_market.type_ == "spot" { None } else { Some("USD".to_string()) }, base, quote, settle: if raw_market.type_ == "spot" { None } else { Some("USD".to_string()) }, active: raw_market.enabled, margin: true, // see https://help.ftx.com/hc/en-us/articles/360024479432-Fees fees: Fees { maker: 0.0002, taker: 0.0007 }, precision: Precision { tick_size: raw_market.priceIncrement, lot_size: raw_market.sizeIncrement, }, quantity_limit: None, contract_value: if raw_market.type_ == "spot" { None } else { Some(1.0) }, delivery_date, info: serde_json::to_value(raw_market).unwrap().as_object().unwrap().clone(), } } fn fetch_spot_markets() -> Result> { let markets: Vec = fetch_markets_raw()? .into_iter() .filter(|x| x.type_ == "spot") .map(|x| to_market(&x)) .collect(); Ok(markets) } fn fetch_linear_swap_markets() -> Result> { let markets = fetch_markets_raw()?; let symbols: Vec = markets .into_iter() .filter(|x| x.type_ == "future" && x.name.ends_with("-PERP")) .map(|x| to_market(&x)) .collect(); Ok(symbols) } fn fetch_linear_future_markets() -> Result> { let markets: Vec = fetch_markets_raw()? .into_iter() .filter(|x| { x.type_ == "future" && !x.name.ends_with("-PERP") && !x.name.contains("-MOVE-") && x.name[(x.name.len() - 4)..].parse::().is_ok() && x.name.contains('-') }) .map(|x| to_market(&x)) .collect(); Ok(markets) } fn fetch_move_markets() -> Result> { let markets: Vec = fetch_markets_raw()? .into_iter() .filter(|x| x.type_ == "future" && x.name.contains("-MOVE-")) .map(|x| to_market(&x)) .collect(); Ok(markets) } fn fetch_bvol_markets() -> Result> { let markets: Vec = fetch_markets_raw()? .into_iter() .filter(|x| x.type_ == "spot" && x.name.contains("BVOL/")) .map(|x| to_market(&x)) .collect(); Ok(markets) } ================================================ FILE: crypto-markets/src/exchanges/gate/gate_future.rs ================================================ use std::collections::HashMap; use super::super::utils::http_get; use crate::{error::Result, Fees, Market, Precision, QuantityLimit}; use crypto_market_type::MarketType; use serde::{Deserialize, Serialize}; use serde_json::Value; // https://www.gateio.pro/docs/apiv4/zh_CN/#deliverycontract #[derive(Clone, Serialize, Deserialize)] #[allow(non_snake_case)] struct FutureMarket { name: String, underlying: String, cycle: String, #[serde(rename = "type")] type_: String, // inverse, direct quanto_multiplier: String, leverage_min: String, leverage_max: String, maintenance_rate: String, mark_type: String, // internal, index maker_fee_rate: String, taker_fee_rate: String, order_price_round: String, mark_price_round: String, settle_fee_rate: String, expire_time: u64, order_size_min: f64, order_size_max: f64, in_delisting: bool, #[serde(flatten)] extra: HashMap, } // See https://www.gateio.pro/docs/apiv4/zh_CN/index.html#595cd9fe3c-2 fn fetch_future_markets_raw(settle: &str) -> Result> { let txt = http_get( format!("https://api.gateio.ws/api/v4/delivery/{settle}/contracts").as_str(), None, )?; let markets = serde_json::from_str::>(&txt)?; Ok(markets.into_iter().filter(|x| !x.in_delisting).collect::>()) } pub(super) fn fetch_inverse_future_symbols() -> Result> { let symbols = fetch_future_markets_raw("btc")?.into_iter().map(|m| m.name).collect::>(); Ok(symbols) } pub(super) fn fetch_linear_future_symbols() -> Result> { let symbols = fetch_future_markets_raw("usdt")?.into_iter().map(|m| m.name).collect::>(); Ok(symbols) } fn to_market(raw_market: &FutureMarket) -> Market { let pair = crypto_pair::normalize_pair(&raw_market.name, "gate").unwrap(); let (base, quote) = { let v: Vec<&str> = pair.split('/').collect(); (v[0].to_string(), v[1].to_string()) }; let (base_id, quote_id) = { let v: Vec<&str> = raw_market.underlying.split('_').collect(); (v[0].to_string(), v[1].to_string()) }; let market_type = if raw_market.type_ == "inverse" { MarketType::InverseFuture } else if raw_market.type_ == "direct" { MarketType::LinearFuture } else { panic!("Unsupported type: {}", raw_market.type_); }; let mut quanto_multiplier = raw_market.quanto_multiplier.parse::().unwrap(); if raw_market.underlying == "BTC_USD" { assert_eq!(quanto_multiplier, 0.0); quanto_multiplier = 1.0; } assert!(quanto_multiplier > 0.0); Market { exchange: "gate".to_string(), market_type, symbol: raw_market.name.to_string(), base_id: base_id.clone(), quote_id: quote_id.clone(), settle_id: if market_type == MarketType::InverseFuture { Some(base_id) } else { Some(quote_id) }, base: base.clone(), quote: quote.clone(), settle: if market_type == MarketType::InverseFuture { Some(base) } else { Some(quote) }, active: !raw_market.in_delisting, margin: true, fees: Fees { maker: raw_market.maker_fee_rate.parse::().unwrap(), taker: raw_market.taker_fee_rate.parse::().unwrap(), }, precision: Precision { tick_size: raw_market.order_price_round.parse::().unwrap(), lot_size: quanto_multiplier, }, quantity_limit: Some(QuantityLimit { min: Some(raw_market.order_size_min), max: Some(raw_market.order_size_max), notional_min: None, notional_max: None, }), contract_value: Some(quanto_multiplier), delivery_date: Some(raw_market.expire_time * 1000), info: serde_json::to_value(raw_market).unwrap().as_object().unwrap().clone(), } } pub(super) fn fetch_inverse_future_markets() -> Result> { let markets = fetch_future_markets_raw("btc")? .into_iter() .map(|m| to_market(&m)) .collect::>(); Ok(markets) } pub(super) fn fetch_linear_future_markets() -> Result> { let markets = fetch_future_markets_raw("usdt")? .into_iter() .map(|m| to_market(&m)) .collect::>(); Ok(markets) } ================================================ FILE: crypto-markets/src/exchanges/gate/gate_spot.rs ================================================ use std::collections::HashMap; use super::super::utils::http_get; use crate::{error::Result, Fees, Market, Precision, QuantityLimit}; use crypto_market_type::MarketType; use serde::{Deserialize, Serialize}; use serde_json::Value; // https://www.gateio.pro/docs/apiv4/zh_CN/#currencypair #[derive(Serialize, Deserialize)] #[allow(non_snake_case)] struct SpotMarket { id: String, base: String, quote: String, fee: String, min_base_amount: Option, min_quote_amount: Option, amount_precision: i64, precision: i64, trade_status: String, #[serde(flatten)] extra: HashMap, } // See https://www.gateio.pro/docs/apiv4/zh_CN/index.html#611e43ef81 fn fetch_spot_markets_raw() -> Result> { let txt = http_get("https://api.gateio.ws/api/v4/spot/currency_pairs", None)?; let markets = serde_json::from_str::>(&txt)?; Ok(markets.into_iter().filter(|x| x.trade_status == "tradable").collect::>()) } pub(super) fn fetch_spot_symbols() -> Result> { let markets = fetch_spot_markets_raw()?; let symbols: Vec = markets.into_iter().map(|m| m.id).collect(); Ok(symbols) } pub(super) fn fetch_spot_markets() -> Result> { let markets: Vec = fetch_spot_markets_raw()? .into_iter() .map(|raw_market| { let info = serde_json::to_value(&raw_market).unwrap().as_object().unwrap().clone(); let pair = crypto_pair::normalize_pair(&raw_market.id, "gate").unwrap(); let (base, quote) = { let v: Vec<&str> = pair.split('/').collect(); (v[0].to_string(), v[1].to_string()) }; Market { exchange: "gate".to_string(), market_type: MarketType::Spot, symbol: raw_market.id.to_string(), base_id: raw_market.base, settle_id: None, quote_id: raw_market.quote, base, quote, settle: None, active: raw_market.trade_status == "tradable", margin: false, fees: Fees { maker: raw_market.fee.parse::().unwrap() / 100_f64, taker: raw_market.fee.parse::().unwrap() / 100_f64, }, precision: Precision { tick_size: 1.0 / (10_i64.pow(raw_market.precision as u32) as f64), lot_size: 1.0 / (10_i64.pow(raw_market.amount_precision as u32) as f64), }, quantity_limit: raw_market.min_base_amount.map(|min_base_amount| QuantityLimit { min: min_base_amount.parse::().ok(), max: None, notional_min: None, notional_max: None, }), contract_value: None, delivery_date: None, info, } }) .collect(); Ok(markets) } ================================================ FILE: crypto-markets/src/exchanges/gate/gate_swap.rs ================================================ use std::collections::HashMap; use super::super::utils::http_get; use crate::{error::Result, Fees, Market, Precision, QuantityLimit}; use crypto_market_type::MarketType; use serde::{Deserialize, Serialize}; use serde_json::Value; // https://www.gateio.pro/docs/apiv4/zh_CN/#contract #[derive(Clone, Serialize, Deserialize)] #[allow(non_snake_case)] struct SwapMarket { name: String, #[serde(rename = "type")] type_: String, // inverse, direct quanto_multiplier: String, leverage_min: String, leverage_max: String, maintenance_rate: String, mark_type: String, // internal, index maker_fee_rate: String, taker_fee_rate: String, order_price_round: String, mark_price_round: String, funding_rate: String, order_size_min: f64, order_size_max: f64, in_delisting: bool, #[serde(flatten)] extra: HashMap, } // See https://www.gateio.pro/docs/apiv4/zh_CN/index.html#595cd9fe3c fn fetch_swap_markets_raw(settle: &str) -> Result> { let txt = http_get( format!("https://api.gateio.ws/api/v4/futures/{settle}/contracts").as_str(), None, )?; let markets = serde_json::from_str::>(&txt)?; Ok(markets.into_iter().filter(|x| !x.in_delisting).collect::>()) } pub(super) fn fetch_inverse_swap_symbols() -> Result> { let symbols = fetch_swap_markets_raw("btc")?.into_iter().map(|m| m.name).collect::>(); Ok(symbols) } pub(super) fn fetch_linear_swap_symbols() -> Result> { let symbols = fetch_swap_markets_raw("usdt")?.into_iter().map(|m| m.name).collect::>(); Ok(symbols) } fn to_market(raw_market: &SwapMarket) -> Market { let pair = crypto_pair::normalize_pair(&raw_market.name, "gate").unwrap(); let (base, quote) = { let v: Vec<&str> = pair.split('/').collect(); (v[0].to_string(), v[1].to_string()) }; let (base_id, quote_id) = { let v: Vec<&str> = raw_market.name.split('_').collect(); (v[0].to_string(), v[1].to_string()) }; let market_type = if raw_market.name.ends_with("_USD") { MarketType::InverseSwap } else if raw_market.name.ends_with("_USDT") { MarketType::LinearSwap } else { panic!("Failed to detect market type for {}", raw_market.name); }; let mut quanto_multiplier = raw_market.quanto_multiplier.parse::().unwrap(); if raw_market.name == "BTC_USD" { assert_eq!(quanto_multiplier, 0.0); quanto_multiplier = 1.0; } assert!(quanto_multiplier > 0.0); Market { exchange: "gate".to_string(), market_type, symbol: raw_market.name.to_string(), base_id: base_id.clone(), quote_id: quote_id.clone(), settle_id: if market_type == MarketType::InverseSwap { Some(base_id) } else { Some(quote_id) }, base: base.clone(), quote: quote.clone(), settle: if market_type == MarketType::InverseSwap { Some(base) } else { Some(quote) }, active: !raw_market.in_delisting, margin: true, fees: Fees { maker: raw_market.maker_fee_rate.parse::().unwrap(), taker: raw_market.taker_fee_rate.parse::().unwrap(), }, precision: Precision { tick_size: raw_market.order_price_round.parse::().unwrap(), lot_size: quanto_multiplier, }, quantity_limit: Some(QuantityLimit { min: Some(raw_market.order_size_min), max: Some(raw_market.order_size_max), notional_min: None, notional_max: None, }), contract_value: Some(quanto_multiplier), delivery_date: None, info: serde_json::to_value(raw_market).unwrap().as_object().unwrap().clone(), } } pub(super) fn fetch_inverse_swap_markets() -> Result> { let markets = fetch_swap_markets_raw("btc")?.into_iter().map(|m| to_market(&m)).collect::>(); Ok(markets) } pub(super) fn fetch_linear_swap_markets() -> Result> { let markets = fetch_swap_markets_raw("usdt")?.into_iter().map(|m| to_market(&m)).collect::>(); Ok(markets) } ================================================ FILE: crypto-markets/src/exchanges/gate/mod.rs ================================================ mod gate_future; mod gate_spot; mod gate_swap; use crate::{error::Result, Market, MarketType}; pub(crate) fn fetch_symbols(market_type: MarketType) -> Result> { match market_type { MarketType::Spot => gate_spot::fetch_spot_symbols(), MarketType::InverseSwap => gate_swap::fetch_inverse_swap_symbols(), MarketType::LinearSwap => gate_swap::fetch_linear_swap_symbols(), MarketType::InverseFuture => gate_future::fetch_inverse_future_symbols(), MarketType::LinearFuture => gate_future::fetch_linear_future_symbols(), _ => panic!("Unsupported market_type: {market_type}"), } } pub(crate) fn fetch_markets(market_type: MarketType) -> Result> { match market_type { MarketType::Spot => gate_spot::fetch_spot_markets(), MarketType::InverseSwap => gate_swap::fetch_inverse_swap_markets(), MarketType::LinearSwap => gate_swap::fetch_linear_swap_markets(), MarketType::InverseFuture => gate_future::fetch_inverse_future_markets(), MarketType::LinearFuture => gate_future::fetch_linear_future_markets(), _ => panic!("Unsupported market_type: {market_type}"), } } ================================================ FILE: crypto-markets/src/exchanges/huobi/huobi_future.rs ================================================ use super::utils::huobi_http_get; use crate::{ error::Result, market::{Fees, Precision}, Market, }; use crypto_market_type::MarketType; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; // https://github.com/ccxt/ccxt/issues/8074 #[derive(Serialize, Deserialize)] struct FutureMarket { symbol: String, contract_code: String, contract_type: String, contract_size: f64, price_tick: f64, delivery_date: String, delivery_time: String, create_date: String, contract_status: i64, settlement_time: String, #[serde(flatten)] extra: HashMap, } #[derive(Serialize, Deserialize)] struct Response { status: String, data: Vec, ts: i64, } // see fn fetch_future_markets_raw() -> Result> { let txt = huobi_http_get("https://api.hbdm.com/api/v1/contract_contract_info")?; let resp = serde_json::from_str::(&txt)?; let result: Vec = resp.data.into_iter().filter(|m| m.contract_status == 1).collect(); Ok(result) } pub(super) fn fetch_inverse_future_symbols() -> Result> { let symbols = fetch_future_markets_raw()? .into_iter() .map(|m| { m.symbol.to_string() + match m.contract_type.as_str() { "this_week" => "_CW", "next_week" => "_NW", "quarter" => "_CQ", "next_quarter" => "_NQ", contract_type => panic!("Unknown contract_type {contract_type}"), } }) .collect::>(); Ok(symbols) } pub(super) fn fetch_inverse_future_markets() -> Result> { let markets = fetch_future_markets_raw()? .into_iter() .map(|m| { let symbol = m.symbol.to_string() + match m.contract_type.as_str() { "this_week" => "_CW", "next_week" => "_NW", "quarter" => "_CQ", "next_quarter" => "_NQ", contract_type => panic!("Unknown contract_type {contract_type}"), }; let pair = crypto_pair::normalize_pair(&symbol, "huobi").unwrap(); let (base, quote) = { let v: Vec<&str> = pair.split('/').collect(); (v[0].to_string(), v[1].to_string()) }; Market { exchange: "huobi".to_string(), market_type: MarketType::InverseFuture, symbol, base_id: m.symbol.to_string(), quote_id: "USD".to_string(), settle_id: Some(m.symbol.to_string()), base: base.clone(), quote, settle: Some(base), active: m.contract_status == 1, margin: true, // see https://futures.huobi.com/en-us/contract/fee_rate/ fees: Fees { maker: 0.0002, taker: 0.0004 }, precision: Precision { tick_size: m.price_tick, lot_size: 1.0 }, quantity_limit: None, contract_value: Some(m.contract_size), delivery_date: Some(m.delivery_time.parse::().unwrap()), info: serde_json::to_value(&m).unwrap().as_object().unwrap().clone(), } }) .collect::>(); Ok(markets) } ================================================ FILE: crypto-markets/src/exchanges/huobi/huobi_inverse_swap.rs ================================================ use super::utils::huobi_http_get; use crate::{ error::Result, market::{Fees, Precision}, Market, }; use crypto_market_type::MarketType; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; #[derive(Serialize, Deserialize)] struct InverseSwapMarket { symbol: String, contract_code: String, contract_size: f64, price_tick: f64, delivery_time: String, create_date: String, contract_status: i64, settlement_date: String, #[serde(flatten)] extra: HashMap, } #[derive(Serialize, Deserialize)] struct Response { status: String, data: Vec, ts: i64, } // see fn fetch_inverse_swap_markets_raw() -> Result> { let txt = huobi_http_get("https://api.hbdm.com/swap-api/v1/swap_contract_info")?; let resp = serde_json::from_str::(&txt)?; let result: Vec = resp.data.into_iter().filter(|m| m.contract_status == 1).collect(); Ok(result) } pub(super) fn fetch_inverse_swap_symbols() -> Result> { let symbols = fetch_inverse_swap_markets_raw()? .into_iter() .map(|m| m.contract_code) .collect::>(); Ok(symbols) } pub(super) fn fetch_inverse_swap_markets() -> Result> { let markets = fetch_inverse_swap_markets_raw()? .into_iter() .map(|m| { let info = serde_json::to_value(&m).unwrap().as_object().unwrap().clone(); let pair = crypto_pair::normalize_pair(&m.contract_code, "huobi").unwrap(); let (base, quote) = { let v: Vec<&str> = pair.split('/').collect(); (v[0].to_string(), v[1].to_string()) }; Market { exchange: "huobi".to_string(), market_type: MarketType::InverseSwap, symbol: m.contract_code, base_id: m.symbol.to_string(), quote_id: "USD".to_string(), settle_id: Some(m.symbol.to_string()), base: base.clone(), quote, settle: Some(base), active: m.contract_status == 1, margin: true, // see https://futures.huobi.com/en-us/swap/fee_rate/ fees: Fees { maker: 0.0002, taker: 0.0005 }, precision: Precision { tick_size: m.price_tick, lot_size: 1.0 }, quantity_limit: None, contract_value: Some(m.contract_size), delivery_date: None, info, } }) .collect::>(); Ok(markets) } ================================================ FILE: crypto-markets/src/exchanges/huobi/huobi_linear_swap.rs ================================================ use super::utils::huobi_http_get; use crate::{ error::Result, market::{Fees, Precision}, Market, }; use crypto_market_type::MarketType; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; #[derive(Serialize, Deserialize)] struct LinearSwapMarket { symbol: String, contract_code: String, contract_size: f64, price_tick: f64, delivery_time: String, create_date: String, contract_status: i64, settlement_date: String, #[serde(flatten)] extra: HashMap, } #[derive(Serialize, Deserialize)] struct Response { status: String, data: Vec, ts: i64, } // see fn fetch_linear_swap_markets_raw() -> Result> { let txt = huobi_http_get("https://api.hbdm.com/linear-swap-api/v1/swap_contract_info")?; let resp = serde_json::from_str::(&txt)?; let result: Vec = resp.data.into_iter().filter(|m| m.contract_status == 1).collect(); Ok(result) } pub(super) fn fetch_linear_swap_symbols() -> Result> { let symbols = fetch_linear_swap_markets_raw()? .into_iter() .map(|m| m.contract_code) .collect::>(); Ok(symbols) } pub(super) fn fetch_linear_swap_markets() -> Result> { let markets = fetch_linear_swap_markets_raw()? .into_iter() .map(|m| { let info = serde_json::to_value(&m).unwrap().as_object().unwrap().clone(); let pair = crypto_pair::normalize_pair(&m.contract_code, "huobi").unwrap(); let (base, quote) = { let v: Vec<&str> = pair.split('/').collect(); (v[0].to_string(), v[1].to_string()) }; Market { exchange: "huobi".to_string(), market_type: MarketType::LinearSwap, symbol: m.contract_code, base_id: m.symbol.to_string(), quote_id: "USDT".to_string(), settle_id: Some("USDT".to_string()), base, quote: quote.clone(), settle: Some(quote), active: m.contract_status == 1, margin: true, // see https://futures.huobi.com/en-us/linear_swap/fee_rate/ fees: Fees { maker: 0.0002, taker: 0.0004 }, precision: Precision { tick_size: m.price_tick, lot_size: 1.0 }, quantity_limit: None, contract_value: Some(m.contract_size), delivery_date: None, info, } }) .collect::>(); Ok(markets) } ================================================ FILE: crypto-markets/src/exchanges/huobi/huobi_option.rs ================================================ use super::utils::huobi_http_get; use crate::error::Result; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; #[derive(Serialize, Deserialize)] struct OptionMarket { symbol: String, contract_code: String, contract_type: String, contract_size: f64, price_tick: f64, delivery_date: String, create_date: String, contract_status: i64, option_right_type: String, exercise_price: f64, delivery_asset: String, quote_asset: String, trade_partition: String, #[serde(flatten)] extra: HashMap, } #[derive(Serialize, Deserialize)] struct Response { status: String, data: Vec, ts: i64, } // see fn fetch_option_markets_raw() -> Result> { let txt = huobi_http_get("https://api.hbdm.com/option-api/v1/option_contract_info")?; let resp = serde_json::from_str::(&txt)?; let result: Vec = resp.data.into_iter().filter(|m| m.contract_status == 1).collect(); Ok(result) } pub(super) fn fetch_option_symbols() -> Result> { let symbols = fetch_option_markets_raw()? .into_iter() .filter(|m| m.contract_status == 1) .map(|m| m.contract_code) .collect::>(); Ok(symbols) } ================================================ FILE: crypto-markets/src/exchanges/huobi/huobi_spot.rs ================================================ use super::utils::huobi_http_get; use crate::{ error::Result, market::{Fees, Precision, QuantityLimit}, Market, }; use crypto_market_type::MarketType; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; #[derive(Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] struct SpotMarket { base_currency: String, quote_currency: String, price_precision: f64, amount_precision: f64, symbol_partition: String, symbol: String, state: String, value_precision: f64, min_order_amt: f64, max_order_amt: f64, min_order_value: f64, limit_order_min_order_amt: f64, limit_order_max_order_amt: f64, sell_market_min_order_amt: f64, sell_market_max_order_amt: f64, buy_market_max_order_value: f64, api_trading: String, #[serde(flatten)] extra: HashMap, } #[derive(Serialize, Deserialize)] struct Response { status: String, data: Vec, } // see fn fetch_spot_markets_raw() -> Result> { let txt = huobi_http_get("https://api.huobi.pro/v1/common/symbols")?; let resp = serde_json::from_str::(&txt)?; let result: Vec = resp.data.into_iter().filter(|m| m.state == "online").collect(); Ok(result) } pub(super) fn fetch_spot_symbols() -> Result> { let symbols = fetch_spot_markets_raw()?.into_iter().map(|m| m.symbol).collect::>(); Ok(symbols) } pub(super) fn fetch_spot_markets() -> Result> { let markets = fetch_spot_markets_raw()? .into_iter() .map(|m| { let info = serde_json::to_value(&m).unwrap().as_object().unwrap().clone(); let pair = crypto_pair::normalize_pair(&m.symbol, "huobi").unwrap(); let (base, quote) = { let v: Vec<&str> = pair.split('/').collect(); (v[0].to_string(), v[1].to_string()) }; Market { exchange: "huobi".to_string(), market_type: MarketType::Spot, symbol: m.symbol, base_id: m.base_currency.to_string(), quote_id: m.quote_currency.to_string(), settle_id: None, base, quote, settle: None, active: m.state == "online", margin: true, // see https://www.huobi.com/en-us/fee/ fees: Fees { maker: 0.002, taker: 0.002 }, precision: Precision { tick_size: 1.0 / (10_i64.pow(m.price_precision as u32) as f64), lot_size: 1.0 / (10_i64.pow(m.amount_precision as u32) as f64), }, quantity_limit: Some(QuantityLimit { min: Some(m.limit_order_min_order_amt), max: Some(m.limit_order_max_order_amt), notional_min: None, notional_max: None, }), contract_value: None, delivery_date: None, info, } }) .collect::>(); Ok(markets) } ================================================ FILE: crypto-markets/src/exchanges/huobi/mod.rs ================================================ mod utils; pub(super) mod huobi_future; pub(super) mod huobi_inverse_swap; pub(super) mod huobi_linear_swap; pub(super) mod huobi_option; pub(super) mod huobi_spot; use crate::{error::Result, Market, MarketType}; pub(crate) fn fetch_symbols(market_type: MarketType) -> Result> { match market_type { MarketType::Spot => huobi_spot::fetch_spot_symbols(), MarketType::InverseFuture => huobi_future::fetch_inverse_future_symbols(), MarketType::InverseSwap => huobi_inverse_swap::fetch_inverse_swap_symbols(), MarketType::LinearSwap => huobi_linear_swap::fetch_linear_swap_symbols(), MarketType::EuropeanOption => huobi_option::fetch_option_symbols(), _ => panic!("Unsupported market_type: {market_type}"), } } pub(crate) fn fetch_markets(market_type: MarketType) -> Result> { match market_type { MarketType::Spot => huobi_spot::fetch_spot_markets(), MarketType::InverseFuture => huobi_future::fetch_inverse_future_markets(), MarketType::InverseSwap => huobi_inverse_swap::fetch_inverse_swap_markets(), MarketType::LinearSwap => huobi_linear_swap::fetch_linear_swap_markets(), MarketType::EuropeanOption => Ok(Vec::new()), _ => panic!("Unsupported market_type: {market_type}"), } } ================================================ FILE: crypto-markets/src/exchanges/huobi/utils.rs ================================================ use super::super::utils::http_get; use crate::error::{Error, Result}; use serde_json::Value; use std::collections::HashMap; fn check_status_in_body(resp: String) -> Result { let obj = serde_json::from_str::>(&resp); if obj.is_err() { return Ok(resp); } match obj.unwrap().get("status") { Some(status) => { if status.as_str().unwrap() != "ok" { Err(Error(resp)) } else { Ok(resp) } } None => Ok(resp), } } pub(super) fn huobi_http_get(url: &str) -> Result { let ret = http_get(url, None); match ret { Ok(resp) => check_status_in_body(resp), Err(_) => ret, } } ================================================ FILE: crypto-markets/src/exchanges/kraken/kraken_futures.rs ================================================ use std::collections::HashMap; use super::super::utils::http_get; use crate::{ error::{Error, Result}, Fees, Market, MarketType, Precision, QuantityLimit, }; use chrono::DateTime; use serde::{Deserialize, Serialize}; use serde_json::Value; #[derive(Clone, Serialize, Deserialize)] struct FuturesMarketPartial { symbol: String, #[serde(rename = "type")] type_: String, tradeable: bool, #[serde(flatten)] extra: HashMap, } #[derive(Clone, Serialize, Deserialize)] #[allow(non_snake_case)] struct FuturesMarket { symbol: String, #[serde(rename = "type")] type_: String, tradeable: bool, underlying: Option, lastTradingTime: Option, // only applicable for futures tickSize: f64, contractSize: f64, isin: Option, #[serde(flatten)] extra: HashMap, } #[derive(Serialize, Deserialize)] struct Response { result: String, instruments: Vec, } fn check_error_in_body(resp: String) -> Result { let obj = serde_json::from_str::>(&resp); if obj.is_err() { return Err(Error(resp)); } let obj = obj.unwrap(); if obj.get("result").unwrap() != "success" { return Err(Error(resp)); } Ok(resp) } pub(super) fn kraken_http_get(url: &str) -> Result { let ret = http_get(url, None); match ret { Ok(resp) => check_error_in_body(resp), Err(_) => ret, } } // see fn fetch_futures_markets_raw() -> Result> { let txt = kraken_http_get("https://futures.kraken.com/derivatives/api/v3/instruments")?; let obj = serde_json::from_str::>(&txt)?; let markets = obj .instruments .into_iter() .filter(|x| x.tradeable) .map(|x| { serde_json::from_str::(serde_json::to_string(&x).unwrap().as_str()) .unwrap() }) .collect::>(); Ok(markets) } pub(super) fn fetch_inverse_future_symbols() -> Result> { let symbols = fetch_futures_markets_raw()? .into_iter() .filter(|x| x.symbol.starts_with("fi_")) .map(|m| m.symbol.to_uppercase()) .collect::>(); Ok(symbols) } pub(super) fn fetch_inverse_swap_symbols() -> Result> { let symbols = fetch_futures_markets_raw()? .into_iter() .filter(|x| x.symbol.starts_with("pi_")) .map(|m| m.symbol.to_uppercase()) .collect::>(); Ok(symbols) } pub(super) fn fetch_inverse_future_markets() -> Result> { let markets = fetch_futures_markets()? .into_iter() .filter(|x| x.market_type == MarketType::InverseFuture) .collect::>(); Ok(markets) } pub(super) fn fetch_inverse_swap_markets() -> Result> { let markets = fetch_futures_markets()? .into_iter() .filter(|x| x.market_type == MarketType::InverseSwap) .collect::>(); Ok(markets) } fn fetch_futures_markets() -> Result> { let markets = fetch_futures_markets_raw()? .into_iter() .filter(|m| m.symbol.starts_with("pi_") || m.symbol.starts_with("fi_")) // TODO: Multi-Collateral, e.g., pf_xbtusd .map(|m| { let info = serde_json::to_value(&m).unwrap().as_object().unwrap().clone(); let pair = crypto_pair::normalize_pair(&m.symbol, "kraken").unwrap(); let (base, quote) = { let v: Vec<&str> = pair.split('/').collect(); (v[0].to_string(), v[1].to_string()) }; let (base_id, quote_id) = { let pos = m.symbol.find("usd").unwrap(); (m.symbol[3..pos].to_string(), "usd".to_string()) }; Market { exchange: "kraken".to_string(), market_type: if m.symbol.starts_with("fi_") { MarketType::InverseFuture } else if m.symbol.starts_with("pi_") { MarketType::InverseSwap } else { MarketType::Unknown }, symbol: m.symbol, base_id: base_id.clone(), quote_id, settle_id: Some(base_id), base: base.clone(), quote, settle: Some(base), active: m.tradeable, margin: true, // see https://futures.kraken.com/derivatives/api/v3/feeschedules fees: Fees { maker: 0.0002, taker: 0.0005 }, precision: Precision { tick_size: m.tickSize, lot_size: 1.0 }, quantity_limit: Some(QuantityLimit { min: Some(1.0), max: None, notional_min: None, notional_max: None, }), contract_value: Some(m.contractSize), delivery_date: m .lastTradingTime .map(|x| DateTime::parse_from_rfc3339(&x).unwrap().timestamp_millis() as u64), info, } }) .collect::>(); Ok(markets) } ================================================ FILE: crypto-markets/src/exchanges/kraken/kraken_spot.rs ================================================ use std::collections::HashMap; use super::super::utils::http_get; use crate::{ error::{Error, Result}, Fees, Market, MarketType, Precision, QuantityLimit, }; use serde::{Deserialize, Serialize}; use serde_json::Value; #[derive(Clone, Serialize, Deserialize)] struct SpotMarket { altname: String, wsname: Option, aclass_base: String, base: String, aclass_quote: String, quote: String, lot: String, pair_decimals: i64, lot_decimals: i64, lot_multiplier: i64, fees: Vec>, fees_maker: Vec>, fee_volume_currency: String, margin_call: i64, margin_stop: i64, ordermin: String, } #[derive(Serialize, Deserialize)] struct Response { result: HashMap, } fn check_error_in_body(resp: String) -> Result { let obj = serde_json::from_str::>(&resp); if obj.is_err() { return Err(Error(resp)); } match obj.unwrap().get("error") { Some(err) => { let arr = err.as_array().unwrap(); if arr.is_empty() { Ok(resp) } else { Err(Error(resp)) } } None => Ok(resp), } } pub(super) fn kraken_http_get(url: &str) -> Result { let ret = http_get(url, None); match ret { Ok(resp) => check_error_in_body(resp), Err(_) => ret, } } // see fn fetch_spot_markets_raw() -> Result> { let txt = kraken_http_get("https://api.kraken.com/0/public/AssetPairs")?; let obj = serde_json::from_str::>(&txt)?; let markets = obj .get("result") .unwrap() .as_object() .unwrap() .values() .filter(|x| x.as_object().unwrap().contains_key("wsname")) .map(|x| serde_json::from_value::(x.clone()).unwrap()) .collect::>(); Ok(markets) } pub(super) fn fetch_spot_symbols() -> Result> { let symbols = fetch_spot_markets_raw()?.into_iter().filter_map(|m| m.wsname).collect::>(); Ok(symbols) } pub(super) fn fetch_spot_markets() -> Result> { let markets = fetch_spot_markets_raw()? .into_iter() .map(|m| { let info = serde_json::to_value(&m).unwrap().as_object().unwrap().clone(); let symbol = m.wsname.unwrap(); let pair = crypto_pair::normalize_pair(&symbol, "kraken").unwrap(); let (base, quote) = { let v: Vec<&str> = pair.split('/').collect(); (v[0].to_string(), v[1].to_string()) }; Market { exchange: "kraken".to_string(), market_type: MarketType::Spot, symbol, base_id: m.base, quote_id: m.quote, settle_id: None, base, quote, settle: None, active: true, margin: false, // see https://support.kraken.com/hc/en-us/articles/360000526126-What-are-Maker-and-Taker-fees- fees: Fees { maker: 0.0016, taker: 0.0026 }, precision: Precision { tick_size: 1.0 / (10_i64.pow(m.pair_decimals as u32) as f64), lot_size: 1.0 / (10_i64.pow(m.lot_decimals as u32) as f64), }, quantity_limit: Some(QuantityLimit { min: m.ordermin.parse::().ok(), max: None, notional_min: None, notional_max: None, }), contract_value: None, delivery_date: None, info, } }) .collect::>(); Ok(markets) } ================================================ FILE: crypto-markets/src/exchanges/kraken/mod.rs ================================================ mod kraken_futures; mod kraken_spot; use crate::{error::Result, Market, MarketType}; pub(crate) fn fetch_symbols(market_type: MarketType) -> Result> { match market_type { MarketType::Spot => kraken_spot::fetch_spot_symbols(), MarketType::InverseFuture => kraken_futures::fetch_inverse_future_symbols(), MarketType::InverseSwap => kraken_futures::fetch_inverse_swap_symbols(), _ => panic!("Unsupported market_type: {market_type}"), } } pub(crate) fn fetch_markets(market_type: MarketType) -> Result> { match market_type { MarketType::Spot => kraken_spot::fetch_spot_markets(), MarketType::InverseFuture => kraken_futures::fetch_inverse_future_markets(), MarketType::InverseSwap => kraken_futures::fetch_inverse_swap_markets(), _ => panic!("Unsupported market_type: {market_type}"), } } ================================================ FILE: crypto-markets/src/exchanges/kucoin/kucoin_spot.rs ================================================ use std::collections::HashMap; use super::super::utils::http_get; use crate::{ error::{Error, Result}, Fees, Market, Precision, QuantityLimit, }; use crypto_market_type::MarketType; use serde::{Deserialize, Serialize}; use serde_json::Value; #[derive(Clone, Serialize, Deserialize)] #[allow(non_snake_case)] struct SpotMarket { symbol: String, name: String, baseCurrency: String, quoteCurrency: String, feeCurrency: String, market: String, baseMinSize: String, quoteMinSize: String, baseMaxSize: String, quoteMaxSize: String, baseIncrement: String, quoteIncrement: String, priceIncrement: String, priceLimitRate: String, isMarginEnabled: bool, enableTrading: bool, #[serde(flatten)] extra: HashMap, } #[derive(Serialize, Deserialize)] struct Response { code: String, data: Vec, } // See https://docs.kucoin.com/#get-symbols-list fn fetch_spot_markets_raw() -> Result> { let txt = http_get("https://api.kucoin.com/api/v1/symbols", None)?; let resp = serde_json::from_str::(&txt)?; if resp.code != "200000" { Err(Error(txt)) } else { let markets = resp.data.into_iter().filter(|x| x.enableTrading).collect::>(); Ok(markets) } } pub(super) fn fetch_spot_symbols() -> Result> { let markets = fetch_spot_markets_raw()?; let symbols: Vec = markets.into_iter().map(|m| m.symbol).collect(); Ok(symbols) } pub(super) fn fetch_spot_markets() -> Result> { let markets: Vec = fetch_spot_markets_raw()? .into_iter() .map(|m| { let info = serde_json::to_value(&m).unwrap().as_object().unwrap().clone(); let pair = crypto_pair::normalize_pair(&m.symbol, "kucoin").unwrap(); let (base, quote) = { let v: Vec<&str> = pair.split('/').collect(); (v[0].to_string(), v[1].to_string()) }; Market { exchange: "kucoin".to_string(), market_type: MarketType::Spot, symbol: m.symbol, base_id: m.baseCurrency, quote_id: m.quoteCurrency, settle_id: None, base, quote, settle: None, active: m.enableTrading, margin: m.isMarginEnabled, // see https://www.bitstamp.net/fee-schedule/ fees: Fees { maker: 0.005, taker: 0.005 }, precision: Precision { tick_size: m.priceIncrement.parse::().unwrap(), lot_size: m.baseIncrement.parse::().unwrap(), }, quantity_limit: Some(QuantityLimit { min: m.baseMinSize.parse::().ok(), max: Some(m.baseMaxSize.parse::().unwrap()), notional_min: None, notional_max: None, }), contract_value: None, delivery_date: None, info, } }) .collect(); Ok(markets) } ================================================ FILE: crypto-markets/src/exchanges/kucoin/kucoin_swap.rs ================================================ use std::collections::HashMap; use super::super::utils::http_get; use crate::{ error::{Error, Result}, Fees, Market, Precision, }; use crypto_market_type::MarketType; use serde::{Deserialize, Serialize}; use serde_json::Value; #[derive(Clone, Serialize, Deserialize)] #[allow(non_snake_case)] struct SwapMarket { symbol: String, rootSymbol: String, #[serde(rename = "type")] type_: String, expireDate: Option, baseCurrency: String, quoteCurrency: String, settleCurrency: String, maxOrderQty: i64, maxPrice: f64, lotSize: f64, tickSize: f64, indexPriceTickSize: f64, multiplier: f64, initialMargin: f64, maintainMargin: f64, maxRiskLimit: i64, minRiskLimit: i64, riskStep: i64, makerFeeRate: f64, takerFeeRate: f64, takerFixFee: f64, makerFixFee: f64, isDeleverage: bool, isQuanto: bool, isInverse: bool, markMethod: String, fairMethod: Option, status: String, fundingFeeRate: Option, predictedFundingFeeRate: Option, openInterest: String, #[serde(flatten)] extra: HashMap, } #[derive(Serialize, Deserialize)] struct Response { code: String, data: Vec, } // See https://docs.kucoin.com/#get-symbols-list fn fetch_swap_markets_raw() -> Result> { let txt = http_get("https://api-futures.kucoin.com/api/v1/contracts/active", None)?; let resp = serde_json::from_str::(&txt)?; if resp.code != "200000" { Err(Error(txt)) } else { let markets = resp.data.into_iter().filter(|x| x.status == "Open").collect::>(); Ok(markets) } } pub(super) fn fetch_inverse_swap_symbols() -> Result> { let markets = fetch_swap_markets_raw()?; let symbols: Vec = markets .into_iter() .filter(|x| x.isInverse && x.type_ == "FFWCSX") .map(|m| m.symbol) .collect(); Ok(symbols) } pub(super) fn fetch_linear_swap_symbols() -> Result> { let markets = fetch_swap_markets_raw()?; let symbols: Vec = markets .into_iter() .filter(|x| !x.isInverse && x.type_ == "FFWCSX") .map(|m| m.symbol) .collect(); Ok(symbols) } pub(super) fn fetch_inverse_future_symbols() -> Result> { let markets = fetch_swap_markets_raw()?; let symbols: Vec = markets .into_iter() .filter(|x| x.isInverse && x.type_ == "FFICSX") .map(|m| m.symbol) .collect(); Ok(symbols) } fn to_market(raw_market: &SwapMarket) -> Market { let pair = crypto_pair::normalize_pair(&raw_market.symbol, "kucoin").unwrap(); let (base, quote) = { let v: Vec<&str> = pair.split('/').collect(); (v[0].to_string(), v[1].to_string()) }; let market_type = if raw_market.isInverse && raw_market.type_ == "FFWCSX" { MarketType::InverseSwap } else if !raw_market.isInverse && raw_market.type_ == "FFWCSX" { MarketType::LinearSwap } else if raw_market.isInverse && raw_market.type_ == "FFICSX" { MarketType::InverseFuture } else { panic!( "Failed to detect market_type {}", serde_json::to_string_pretty(raw_market).unwrap() ); }; Market { exchange: "kucoin".to_string(), market_type, symbol: raw_market.symbol.to_string(), base_id: raw_market.baseCurrency.to_string(), quote_id: raw_market.quoteCurrency.to_string(), settle_id: if raw_market.isInverse { Some(raw_market.baseCurrency.to_string()) } else { Some(raw_market.quoteCurrency.to_string()) }, base: base.clone(), quote: quote.clone(), settle: if raw_market.isInverse { Some(base) } else { Some(quote) }, active: raw_market.status == "Open", margin: true, fees: Fees { maker: raw_market.makerFeeRate, taker: raw_market.takerFeeRate }, precision: Precision { tick_size: raw_market.tickSize, lot_size: raw_market.lotSize }, quantity_limit: None, contract_value: Some(raw_market.multiplier.abs()), delivery_date: raw_market.expireDate, info: serde_json::to_value(raw_market).unwrap().as_object().unwrap().clone(), } } pub(super) fn fetch_inverse_swap_markets() -> Result> { let markets: Vec = fetch_swap_markets_raw()? .into_iter() .filter(|x| x.isInverse && x.type_ == "FFWCSX") .map(|m| to_market(&m)) .collect(); Ok(markets) } pub(super) fn fetch_linear_swap_markets() -> Result> { let markets: Vec = fetch_swap_markets_raw()? .into_iter() .filter(|x| !x.isInverse && x.type_ == "FFWCSX") .map(|m| to_market(&m)) .collect(); Ok(markets) } pub(super) fn fetch_inverse_future_markets() -> Result> { let markets: Vec = fetch_swap_markets_raw()? .into_iter() .filter(|x| x.isInverse && x.type_ == "FFICSX") .map(|m| to_market(&m)) .collect(); Ok(markets) } ================================================ FILE: crypto-markets/src/exchanges/kucoin/mod.rs ================================================ mod kucoin_spot; mod kucoin_swap; use crate::{error::Result, Market, MarketType}; pub(crate) fn fetch_symbols(market_type: MarketType) -> Result> { match market_type { MarketType::Spot => kucoin_spot::fetch_spot_symbols(), MarketType::InverseSwap => kucoin_swap::fetch_inverse_swap_symbols(), MarketType::LinearSwap => kucoin_swap::fetch_linear_swap_symbols(), MarketType::InverseFuture => kucoin_swap::fetch_inverse_future_symbols(), _ => panic!("Unsupported market_type: {market_type}"), } } pub(crate) fn fetch_markets(market_type: MarketType) -> Result> { match market_type { MarketType::Spot => kucoin_spot::fetch_spot_markets(), MarketType::InverseSwap => kucoin_swap::fetch_inverse_swap_markets(), MarketType::LinearSwap => kucoin_swap::fetch_linear_swap_markets(), MarketType::InverseFuture => kucoin_swap::fetch_inverse_future_markets(), _ => panic!("Unsupported market_type: {market_type}"), } } ================================================ FILE: crypto-markets/src/exchanges/mexc/mexc_spot.rs ================================================ use super::utils::mexc_http_get; use crate::{error::Result, Fees, Market, Precision, QuantityLimit}; use crypto_market_type::MarketType; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; #[derive(Serialize, Deserialize)] struct SpotMarket { symbol: String, state: String, price_scale: u32, quantity_scale: u32, min_amount: String, max_amount: String, maker_fee_rate: String, taker_fee_rate: String, limited: bool, #[serde(flatten)] extra: HashMap, } #[derive(Serialize, Deserialize)] struct Response { code: i64, data: Vec, } // see fn fetch_spot_markets_raw() -> Result> { let txt = mexc_http_get("https://www.mexc.com/open/api/v2/market/symbols")?; let resp = serde_json::from_str::(&txt)?; Ok(resp.data.into_iter().filter(|m| m.state == "ENABLED" && !m.limited).collect()) } pub(super) fn fetch_spot_symbols() -> Result> { let symbols = fetch_spot_markets_raw()?.into_iter().map(|m| m.symbol).collect::>(); Ok(symbols) } pub(super) fn fetch_spot_markets() -> Result> { let markets = fetch_spot_markets_raw()? .into_iter() .map(|m| { let info = serde_json::to_value(&m).unwrap().as_object().unwrap().clone(); let pair = crypto_pair::normalize_pair(&m.symbol, super::EXCHANGE_NAME).unwrap(); let (base, quote) = { let v: Vec<&str> = pair.split('/').collect(); (v[0].to_string(), v[1].to_string()) }; let (base_id, quote_id) = { let v: Vec<&str> = m.symbol.split('_').collect(); (v[0].to_string(), v[1].to_string()) }; Market { exchange: super::EXCHANGE_NAME.to_string(), market_type: MarketType::Spot, symbol: m.symbol, base_id, quote_id, settle_id: None, base, quote, settle: None, active: m.state == "ENABLED" && !m.limited, margin: false, fees: Fees { maker: m.maker_fee_rate.parse::().unwrap(), taker: m.taker_fee_rate.parse::().unwrap(), }, precision: Precision { tick_size: 1.0 / (10_i64.pow(m.price_scale) as f64), lot_size: 1.0 / (10_i64.pow(m.quantity_scale) as f64), }, quantity_limit: Some(QuantityLimit { min: m.min_amount.parse::().ok(), max: Some(m.max_amount.parse::().unwrap()), notional_min: None, notional_max: None, }), contract_value: None, delivery_date: None, info, } }) .collect::>(); Ok(markets) } ================================================ FILE: crypto-markets/src/exchanges/mexc/mexc_swap.rs ================================================ use super::utils::mexc_http_get; use crate::{error::Result, Fees, Market, Precision, QuantityLimit}; use crypto_market_type::MarketType; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; #[derive(Serialize, Deserialize)] #[allow(non_snake_case)] struct SwapMarket { symbol: String, displayName: String, displayNameEn: String, positionOpenType: i64, baseCoin: String, quoteCoin: String, settleCoin: String, contractSize: f64, minLeverage: i64, maxLeverage: i64, priceScale: i64, volScale: i64, amountScale: i64, priceUnit: f64, volUnit: i64, minVol: i64, maxVol: i64, bidLimitPriceRate: f64, askLimitPriceRate: f64, takerFeeRate: f64, makerFeeRate: f64, maintenanceMarginRate: f64, initialMarginRate: f64, state: i64, isNew: bool, isHot: bool, isHidden: bool, #[serde(flatten)] extra: HashMap, } #[derive(Serialize, Deserialize)] struct Response { success: bool, code: i64, data: Vec, } // see fn fetch_swap_markets_raw() -> Result> { let txt = mexc_http_get("https://contract.mexc.com/api/v1/contract/detail")?; let resp = serde_json::from_str::(&txt)?; Ok(resp.data.into_iter().filter(|m| m.state == 0 && !m.isHidden).collect()) } pub(super) fn fetch_linear_swap_symbols() -> Result> { let symbols = fetch_swap_markets_raw()? .into_iter() .filter(|m| m.settleCoin == m.quoteCoin) .map(|m| m.symbol) .collect::>(); Ok(symbols) } pub(super) fn fetch_inverse_swap_symbols() -> Result> { let symbols = fetch_swap_markets_raw()? .into_iter() .filter(|m| m.settleCoin == m.baseCoin) .map(|m| m.symbol) .collect::>(); Ok(symbols) } fn to_market(raw_market: &SwapMarket) -> Market { let pair = crypto_pair::normalize_pair(&raw_market.symbol, super::EXCHANGE_NAME).unwrap(); let (base, quote) = { let v: Vec<&str> = pair.split('/').collect(); (v[0].to_string(), v[1].to_string()) }; let market_type = if raw_market.settleCoin == raw_market.quoteCoin { MarketType::LinearSwap } else if raw_market.settleCoin == raw_market.baseCoin { MarketType::InverseSwap } else { panic!("unexpected market type"); }; Market { exchange: super::EXCHANGE_NAME.to_string(), market_type, symbol: raw_market.symbol.to_string(), base_id: raw_market.baseCoin.to_string(), quote_id: raw_market.quoteCoin.to_string(), settle_id: Some(raw_market.settleCoin.to_string()), base, quote, settle: Some(raw_market.settleCoin.to_string()), active: raw_market.state == 0 && !raw_market.isHidden, margin: true, fees: Fees { maker: raw_market.makerFeeRate, taker: raw_market.takerFeeRate }, precision: Precision { tick_size: raw_market.priceUnit, lot_size: raw_market.volUnit as f64, }, quantity_limit: Some(QuantityLimit { min: Some(raw_market.minVol as f64), max: Some(raw_market.maxVol as f64), notional_min: None, notional_max: None, }), contract_value: Some(raw_market.contractSize), delivery_date: None, info: serde_json::to_value(raw_market).unwrap().as_object().unwrap().clone(), } } pub(super) fn fetch_linear_swap_markets() -> Result> { let markets = fetch_swap_markets_raw()? .into_iter() .filter(|m| m.settleCoin == m.quoteCoin) .map(|m| to_market(&m)) .collect::>(); Ok(markets) } pub(super) fn fetch_inverse_swap_markets() -> Result> { let markets = fetch_swap_markets_raw()? .into_iter() .filter(|m| m.settleCoin == m.baseCoin) .map(|m| to_market(&m)) .collect::>(); Ok(markets) } ================================================ FILE: crypto-markets/src/exchanges/mexc/mod.rs ================================================ mod utils; pub(super) mod mexc_spot; pub(super) mod mexc_swap; use crate::{error::Result, Market, MarketType}; pub(super) const EXCHANGE_NAME: &str = "mexc"; pub(crate) fn fetch_symbols(market_type: MarketType) -> Result> { match market_type { MarketType::Spot => mexc_spot::fetch_spot_symbols(), MarketType::InverseSwap => mexc_swap::fetch_inverse_swap_symbols(), MarketType::LinearSwap => mexc_swap::fetch_linear_swap_symbols(), _ => panic!("Unsupported market_type: {market_type}"), } } pub(crate) fn fetch_markets(market_type: MarketType) -> Result> { match market_type { MarketType::Spot => mexc_spot::fetch_spot_markets(), MarketType::InverseSwap => mexc_swap::fetch_inverse_swap_markets(), MarketType::LinearSwap => mexc_swap::fetch_linear_swap_markets(), _ => panic!("Unsupported market_type: {market_type}"), } } ================================================ FILE: crypto-markets/src/exchanges/mexc/utils.rs ================================================ use super::super::utils::http_get; use crate::error::{Error, Result}; use serde_json::Value; use std::collections::HashMap; fn check_code_in_body(resp: String) -> Result { let obj = serde_json::from_str::>(&resp); if obj.is_err() { return Ok(resp); } match obj.unwrap().get("code") { Some(code) => { let code_int = code.as_i64().unwrap(); if code_int == 0 || code_int == 200 { Ok(resp) } else { Err(Error(resp)) } } None => Ok(resp), } } pub(super) fn mexc_http_get(url: &str) -> Result { let ret = http_get(url, None); match ret { Ok(resp) => check_code_in_body(resp), Err(_) => ret, } } ================================================ FILE: crypto-markets/src/exchanges/mod.rs ================================================ #[macro_use] mod utils; pub(super) mod binance; pub(super) mod bitfinex; pub(super) mod bitget; pub(super) mod bithumb; pub(super) mod bitmex; pub(super) mod bitstamp; pub(super) mod bitz; pub(super) mod bybit; pub(super) mod coinbase_pro; pub(super) mod deribit; pub(super) mod dydx; pub(super) mod ftx; pub(super) mod gate; pub(super) mod huobi; pub(super) mod kraken; pub(super) mod kucoin; pub(super) mod mexc; pub(super) mod okx; pub(super) mod zb; pub(super) mod zbg; ================================================ FILE: crypto-markets/src/exchanges/okx.rs ================================================ use std::collections::HashMap; use super::utils::http_get; use crate::{ error::Result, market::{Fees, Precision, QuantityLimit}, Market, }; // use chrono::DateTime; use crypto_market_type::MarketType; use serde::{Deserialize, Serialize}; use serde_json::Value; pub(crate) fn fetch_symbols(market_type: MarketType) -> Result> { match market_type { MarketType::Spot => fetch_spot_symbols(), MarketType::InverseFuture => fetch_inverse_future_symbols(), MarketType::LinearFuture => fetch_linear_future_symbols(), MarketType::InverseSwap => fetch_inverse_swap_symbols(), MarketType::LinearSwap => fetch_linear_swap_symbols(), MarketType::EuropeanOption => fetch_option_symbols(), _ => panic!("Unsupported market_type: {market_type}"), } } pub(crate) fn fetch_markets(market_type: MarketType) -> Result> { match market_type { MarketType::Spot => fetch_spot_markets(), MarketType::InverseFuture => fetch_inverse_future_markets(), MarketType::LinearFuture => fetch_linear_future_markets(), MarketType::InverseSwap => fetch_inverse_swap_markets(), MarketType::LinearSwap => fetch_linear_swap_markets(), MarketType::EuropeanOption => fetch_option_markets(), _ => panic!("Unsupported market_type: {market_type}"), } } // see #[derive(Serialize, Deserialize)] #[allow(non_snake_case)] struct RawMarket { instType: String, // Instrument type instId: String, // Instrument ID, e.g. BTC-USD-SWAP uly: String, // Underlying, e.g. BTC-USD. Only applicable to FUTURES/SWAP/OPTION category: String, // Fee schedule baseCcy: String, // Base currency, e.g. BTC inBTC-USDT. Only applicable to SPOT quoteCcy: String, // Quote currency, e.g. USDT in BTC-USDT. Only applicable to SPOT settleCcy: String, /* Settlement and margin currency, e.g. BTC. Only applicable to * FUTURES/SWAP/OPTION */ ctVal: String, // Contract value. Only applicable to FUTURES/SWAP/OPTION ctMult: String, // Contract multiplier. Only applicable to FUTURES/SWAP/OPTION ctValCcy: String, // Contract value currency. Only applicable to FUTURES/SWAP/OPTION optType: String, // Option type, C: Call P: put. Only applicable to OPTION stk: String, // Strike price. Only applicable to OPTION listTime: String, // Listing time, Unix timestamp format in milliseconds, e.g. 1597026383085 expTime: String, /* Expiry time, Unix timestamp format in milliseconds, e.g. 1597026383085. * Only applicable to FUTURES/OPTION */ lever: String, // Max Leverage. Not applicable to SPOT、OPTION tickSz: String, // Tick size, e.g. 0.0001 lotSz: String, // Lot size, e.g. BTC-USDT-SWAP: 1 minSz: String, // Minimum order size ctType: String, // Contract type, linear, inverse. Only applicable to FUTURES/SWAP alias: String, /* Alias, this_week, next_week, quarter, next_quarter. Only applicable to * FUTURES */ state: String, // Instrument status, live, suspend, preopen, settlement #[serde(flatten)] extra: HashMap, } impl RawMarket { fn to_market(&self) -> Market { let pair = crypto_pair::normalize_pair(self.instId.as_str(), "okx").unwrap(); let (base, quote) = { let v: Vec<&str> = pair.split('/').collect(); (v[0].to_string(), v[1].to_string()) }; let (market_type, base_id, quote_id) = if self.instType == "SPOT" { (MarketType::Spot, self.baseCcy.clone(), self.quoteCcy.clone()) } else if self.instType == "FUTURES" { if self.ctType == "linear" { (MarketType::LinearFuture, self.ctValCcy.clone(), self.settleCcy.clone()) } else if self.ctType == "inverse" { (MarketType::InverseFuture, self.settleCcy.clone(), self.ctValCcy.clone()) } else { panic!("Unsupported ctType: {}", self.ctType); } } else if self.instType == "SWAP" { if self.ctType == "linear" { (MarketType::LinearSwap, self.ctValCcy.clone(), self.settleCcy.clone()) } else if self.ctType == "inverse" { (MarketType::InverseSwap, self.settleCcy.clone(), self.ctValCcy.clone()) } else { panic!("Unsupported ctType: {}", self.ctType); } } else if self.instType == "OPTION" { (MarketType::EuropeanOption, self.settleCcy.clone(), "USD".to_string()) } else { panic!("Unsupported market_type: {}", self.instType); }; Market { exchange: "okx".to_string(), market_type, symbol: self.instId.to_string(), base_id, quote_id, settle_id: if self.instType == "SPOT" { None } else { Some(self.settleCcy.clone()) }, base, quote, settle: if self.instType == "SPOT" { None } else { Some(self.settleCcy.clone()) }, active: self.state == "live", margin: !self.lever.is_empty(), // see https://www.okx.com/fees.html fees: Fees { maker: if self.instType == "SPOT" { 0.0008 } else { 0.0002 }, taker: if self.instType == "SPOT" { 0.001 } else { 0.0005 }, }, precision: Precision { tick_size: self.tickSz.parse::().unwrap(), lot_size: self.lotSz.parse::().unwrap(), }, quantity_limit: Some(QuantityLimit { min: self.minSz.parse::().ok(), max: None, notional_min: None, notional_max: None, }), contract_value: if self.instType == "SPOT" { None } else { Some(self.ctVal.parse::().unwrap()) }, delivery_date: if self.instType == "FUTURES" || self.instType == "OPTION" { Some(self.expTime.parse::().unwrap()) } else { None }, info: serde_json::to_value(self).unwrap().as_object().unwrap().clone(), } } } // Retrieve a list of instruments. // // see // instType: SPOT, MARGIN, SWAP, FUTURES, OPTION fn fetch_raw_markets_raw(inst_type: &str) -> Result> { let markets = if inst_type == "OPTION" { let underlying_indexes = { let txt = http_get("https://www.okx.com/api/v5/public/underlying?instType=OPTION", None)?; let json_obj = serde_json::from_str::>(&txt).unwrap(); let data = json_obj.get("data").unwrap().as_array().unwrap()[0].as_array().unwrap(); data.iter().map(|x| x.as_str().unwrap().to_string()).collect::>() }; let mut markets = Vec::::new(); for underlying in underlying_indexes.iter() { let url = format!( "https://www.okx.com/api/v5/public/instruments?instType=OPTION&uly={underlying}" ); let txt = { let txt = http_get(url.as_str(), None)?; let json_obj = serde_json::from_str::>(&txt).unwrap(); serde_json::to_string(json_obj.get("data").unwrap()).unwrap() }; let mut arr = serde_json::from_str::>(&txt).unwrap(); markets.append(&mut arr); } markets } else { let url = format!("https://www.okx.com/api/v5/public/instruments?instType={inst_type}"); let txt = { let txt = http_get(url.as_str(), None)?; let json_obj = serde_json::from_str::>(&txt).unwrap(); serde_json::to_string(json_obj.get("data").unwrap()).unwrap() }; serde_json::from_str::>(&txt).unwrap() }; Ok(markets.into_iter().filter(|x| x.state == "live").collect()) } fn fetch_spot_symbols() -> Result> { let symbols = fetch_raw_markets_raw("SPOT")?.into_iter().map(|m| m.instId).collect::>(); Ok(symbols) } fn fetch_inverse_future_symbols() -> Result> { let symbols = fetch_raw_markets_raw("FUTURES")? .into_iter() .filter(|m| m.ctType == "inverse") .map(|m| m.instId) .collect::>(); Ok(symbols) } fn fetch_linear_future_symbols() -> Result> { let symbols = fetch_raw_markets_raw("FUTURES")? .into_iter() .filter(|m| m.ctType == "linear") .map(|m| m.instId) .collect::>(); Ok(symbols) } fn fetch_inverse_swap_symbols() -> Result> { let symbols = fetch_raw_markets_raw("SWAP")? .into_iter() .filter(|m| m.ctType == "inverse") .map(|m| m.instId) .collect::>(); Ok(symbols) } fn fetch_linear_swap_symbols() -> Result> { let symbols = fetch_raw_markets_raw("SWAP")? .into_iter() .filter(|m| m.ctType == "linear") .map(|m| m.instId) .collect::>(); Ok(symbols) } fn fetch_option_symbols() -> Result> { let symbols = fetch_raw_markets_raw("OPTION")?.into_iter().map(|m| m.instId).collect::>(); Ok(symbols) } fn fetch_spot_markets() -> Result> { let markets = fetch_raw_markets_raw("SPOT")?.into_iter().map(|m| m.to_market()).collect::>(); Ok(markets) } fn fetch_inverse_future_markets() -> Result> { let markets = fetch_raw_markets_raw("FUTURES")? .into_iter() .filter(|m| m.ctType == "inverse") .map(|m| m.to_market()) .collect::>(); Ok(markets) } fn fetch_linear_future_markets() -> Result> { let markets = fetch_raw_markets_raw("FUTURES")? .into_iter() .filter(|m| m.ctType == "linear") .map(|m| m.to_market()) .collect::>(); Ok(markets) } fn fetch_inverse_swap_markets() -> Result> { let markets = fetch_raw_markets_raw("SWAP")? .into_iter() .filter(|m| m.ctType == "inverse") .map(|m| m.to_market()) .collect::>(); Ok(markets) } fn fetch_linear_swap_markets() -> Result> { let markets = fetch_raw_markets_raw("SWAP")? .into_iter() .filter(|m| m.ctType == "linear") .map(|m| m.to_market()) .collect::>(); Ok(markets) } fn fetch_option_markets() -> Result> { let markets = fetch_raw_markets_raw("OPTION")? .into_iter() .map(|m| m.to_market()) .collect::>(); Ok(markets) } ================================================ FILE: crypto-markets/src/exchanges/utils.rs ================================================ use reqwest::header; use crate::error::{Error, Result}; use std::collections::HashMap; pub(super) fn http_get(url: &str, params: Option<&HashMap>) -> Result { let mut full_url = url.to_string(); if let Some(params) = params { let mut first = true; for (k, v) in params.iter() { if first { full_url.push_str(format!("?{k}={v}").as_str()); first = false; } else { full_url.push_str(format!("&{k}={v}").as_str()); } } } // println!("{}", full_url); let mut headers = header::HeaderMap::new(); headers.insert(header::CONTENT_TYPE, header::HeaderValue::from_static("application/json")); let client = reqwest::blocking::Client::builder() .default_headers(headers) .user_agent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36") .gzip(true) .build()?; let response = client.get(full_url.as_str()).send()?; match response.error_for_status() { Ok(resp) => Ok(resp.text()?), Err(error) => Err(Error::from(error)), } } #[allow(dead_code)] fn precision_from_string(s: &str) -> i64 { if let Some(dot_pos) = s.find('.') { let mut none_zero = 0; for (i, ch) in s.chars().rev().enumerate() { if ch != '0' { none_zero = s.len() - 1 - i; break; } } (none_zero - dot_pos) as i64 } else { 0 } } #[cfg(test)] mod tests { use std::collections::HashMap; use serde_json::Value; // System proxies are enabled by default, see #[test] #[ignore] fn use_system_socks_proxy() { std::env::set_var("https_proxy", "socks5://127.0.0.1:9050"); let text = super::http_get("https://check.torproject.org/api/ip", None).unwrap(); let obj = serde_json::from_str::>(&text).unwrap(); assert!(obj.get("IsTor").unwrap().as_bool().unwrap()); } #[test] #[ignore] fn use_system_https_proxy() { std::env::set_var("https_proxy", "http://127.0.0.1:8118"); let text = super::http_get("https://check.torproject.org/api/ip", None).unwrap(); let obj = serde_json::from_str::>(&text).unwrap(); assert!(obj.get("IsTor").unwrap().as_bool().unwrap()); } #[test] fn test_calc_precision() { assert_eq!(4, super::precision_from_string("0.000100")); assert_eq!(0, super::precision_from_string("10.00000000")); } } ================================================ FILE: crypto-markets/src/exchanges/zb/mod.rs ================================================ mod zb_spot; mod zb_swap; use crate::{error::Result, Market, MarketType}; pub(crate) fn fetch_symbols(market_type: MarketType) -> Result> { match market_type { MarketType::Spot => zb_spot::fetch_spot_symbols(), MarketType::LinearSwap => zb_swap::fetch_linear_swap_symbols(), _ => panic!("Unkown market_type: {market_type}"), } } pub(crate) fn fetch_markets(market_type: MarketType) -> Result> { match market_type { MarketType::Spot => zb_spot::fetch_spot_markets(), MarketType::LinearSwap => zb_swap::fetch_linear_swap_markets(), _ => panic!("Unkown market_type: {market_type}"), } } ================================================ FILE: crypto-markets/src/exchanges/zb/zb_spot.rs ================================================ use std::collections::HashMap; use super::super::utils::http_get; use crate::{error::Result, Fees, Market, Precision, QuantityLimit}; use crypto_market_type::MarketType; use serde::{Deserialize, Serialize}; use serde_json::Value; #[derive(Serialize, Deserialize)] #[allow(non_snake_case)] struct SpotMarket { #[serde(default)] symbol: String, amountScale: u32, minAmount: f64, minSize: f64, priceScale: u32, #[serde(flatten)] extra: HashMap, } // See https://zbgapi.github.io/docs/spot/v1/en/#public-get-all-supported-trading-symbols fn fetch_spot_markets_raw() -> Result> { let txt = http_get("https://api.zb.com/data/v1/markets", None)?; let m = serde_json::from_str::>(&txt)?; let mut markets = Vec::new(); for (symbol, mut market) in m { market.symbol = symbol; markets.push(market); } Ok(markets) } pub(super) fn fetch_spot_symbols() -> Result> { let markets = fetch_spot_markets_raw()?; let symbols: Vec = markets.into_iter().map(|m| m.symbol).collect(); Ok(symbols) } pub(super) fn fetch_spot_markets() -> Result> { let markets: Vec = fetch_spot_markets_raw()? .into_iter() .map(|m| { let info = serde_json::to_value(&m).unwrap().as_object().unwrap().clone(); let (base_id, quote_id) = { let v: Vec<&str> = m.symbol.split('_').collect(); (v[0].to_string(), v[1].to_string()) }; let pair = crypto_pair::normalize_pair(&m.symbol, "zb").unwrap(); let (base, quote) = { let v: Vec<&str> = pair.split('/').collect(); (v[0].to_string(), v[1].to_string()) }; Market { exchange: "zb".to_string(), market_type: MarketType::Spot, symbol: m.symbol, base_id, quote_id, settle_id: None, base, quote, settle: None, active: true, margin: false, // see https://www.zb.com/help/rate/6 fees: Fees { maker: 0.002, taker: 0.002 }, precision: Precision { tick_size: 1.0 / (10_i64.pow(m.priceScale) as f64), lot_size: 1.0 / (10_i64.pow(m.amountScale) as f64), }, quantity_limit: Some(QuantityLimit { min: Some(m.minAmount), max: None, notional_min: None, notional_max: None, }), contract_value: None, delivery_date: None, info, } }) .collect(); Ok(markets) } ================================================ FILE: crypto-markets/src/exchanges/zb/zb_swap.rs ================================================ use std::collections::HashMap; use super::super::utils::http_get; use crate::{ error::{Error, Result}, Fees, Market, Precision, QuantityLimit, }; use crypto_market_type::MarketType; use serde::{Deserialize, Serialize}; use serde_json::Value; #[derive(Serialize, Deserialize)] #[allow(non_snake_case)] struct SwapMarket { symbol: String, marginCurrencyName: String, buyerCurrencyName: String, sellerCurrencyName: String, canTrade: bool, canOpenPosition: bool, amountDecimal: u32, priceDecimal: u32, minAmount: String, maxAmount: String, status: i64, #[serde(flatten)] extra: HashMap, } #[derive(Serialize, Deserialize)] #[allow(non_snake_case)] struct Response { code: i64, data: Vec, } // See https://github.com/ZBFuture/docs/blob/main/API%20V2%20_en.md#71-trading-pair fn fetch_swap_markets_internal(url: &str) -> Result> { let txt = http_get(url, None)?; let resp = serde_json::from_str::(&txt).unwrap(); if resp.code == 10000 { Ok(resp .data .into_iter() .filter(|m| m.canTrade && m.canOpenPosition && m.status == 1) .collect()) } else { Err(Error(txt)) } } fn fetch_swap_markets_raw() -> Result> { let usdt_markets = fetch_swap_markets_internal("https://fapi.zb.com/Server/api/v2/config/marketList")?; Ok(usdt_markets) // let qc_markets = // fetch_swap_markets_internal("https://fapi.zb.com/qc/Server/api/v2/config/marketList")?; // Ok(usdt_markets // .into_iter() // .chain(qc_markets.into_iter()) // .collect()) } pub(super) fn fetch_linear_swap_symbols() -> Result> { let symbols = fetch_swap_markets_raw()?.into_iter().map(|m| m.symbol).collect::>(); Ok(symbols) } fn to_market(raw_market: &SwapMarket) -> Market { let (base_id, quote_id) = { let v: Vec<&str> = raw_market.symbol.split('_').collect(); (v[0].to_string(), v[1].to_string()) }; let pair = crypto_pair::normalize_pair(&raw_market.symbol, "zb").unwrap(); let (base, quote) = { let v: Vec<&str> = pair.split('/').collect(); (v[0].to_string(), v[1].to_string()) }; Market { exchange: "zb".to_string(), market_type: MarketType::LinearSwap, symbol: raw_market.symbol.to_string(), base_id, quote_id, settle_id: Some(raw_market.marginCurrencyName.to_uppercase()), base, quote, settle: Some(raw_market.marginCurrencyName.to_uppercase()), active: true, margin: true, // see https://www.zb.com/help/rate/20 fees: Fees { maker: 0.0005, taker: 0.00075 }, precision: Precision { tick_size: 1.0 / (10_i64.pow(raw_market.priceDecimal) as f64), lot_size: 1.0 / (10_i64.pow(raw_market.amountDecimal) as f64), }, quantity_limit: Some(QuantityLimit { min: raw_market.minAmount.parse::().ok(), max: Some(raw_market.maxAmount.parse::().unwrap()), notional_min: None, notional_max: None, }), contract_value: Some(1.0), delivery_date: None, info: serde_json::to_value(raw_market).unwrap().as_object().unwrap().clone(), } } pub(super) fn fetch_linear_swap_markets() -> Result> { let markets = fetch_swap_markets_raw()?.into_iter().map(|m| to_market(&m)).collect::>(); Ok(markets) } ================================================ FILE: crypto-markets/src/exchanges/zbg/mod.rs ================================================ mod zbg_spot; mod zbg_swap; use crate::{error::Result, Market, MarketType}; pub(crate) fn fetch_symbols(market_type: MarketType) -> Result> { match market_type { MarketType::Spot => zbg_spot::fetch_spot_symbols(), MarketType::InverseSwap => zbg_swap::fetch_inverse_swap_symbols(), MarketType::LinearSwap => zbg_swap::fetch_linear_swap_symbols(), _ => panic!("Unkown market_type: {market_type}"), } } pub(crate) fn fetch_markets(market_type: MarketType) -> Result> { match market_type { MarketType::Spot => zbg_spot::fetch_spot_markets(), MarketType::InverseSwap => zbg_swap::fetch_inverse_swap_markets(), MarketType::LinearSwap => zbg_swap::fetch_linear_swap_markets(), _ => panic!("Unkown market_type: {market_type}"), } } ================================================ FILE: crypto-markets/src/exchanges/zbg/zbg_spot.rs ================================================ use std::collections::HashMap; use super::super::utils::http_get; use crate::{ error::{Error, Result}, Fees, Market, Precision, QuantityLimit, }; use crypto_market_type::MarketType; use serde::{Deserialize, Serialize}; use serde_json::Value; #[derive(Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] struct SpotMarket { symbol: String, symbol_partition: String, price_precision: i64, min_order_amt: String, id: String, state: String, base_currency: String, amount_precision: i64, max_order_amt: Option, quote_currency: String, #[serde(flatten)] extra: HashMap, } #[derive(Serialize, Deserialize)] struct ResMsg { message: String, method: Option, code: String, } #[derive(Serialize, Deserialize)] #[allow(non_snake_case)] struct Response { datas: Vec, resMsg: ResMsg, } // See https://zbgapi.github.io/docs/spot/v1/en/#public-get-all-supported-trading-symbols fn fetch_spot_markets_raw() -> Result> { let txt = http_get("https://www.zbg.com/exchange/api/v1/common/symbols", None)?; let resp = serde_json::from_str::(&txt)?; if resp.resMsg.code != "1" { Err(Error(txt)) } else { let valid: Vec = resp.datas.into_iter().filter(|x| x.state == "online").collect(); Ok(valid) } } pub(super) fn fetch_spot_symbols() -> Result> { let markets = fetch_spot_markets_raw()?; let symbols: Vec = markets.into_iter().map(|m| m.symbol).collect(); Ok(symbols) } pub(super) fn fetch_spot_markets() -> Result> { let markets: Vec = fetch_spot_markets_raw()? .into_iter() .map(|m| { let info = serde_json::to_value(&m).unwrap().as_object().unwrap().clone(); let pair = crypto_pair::normalize_pair(&m.symbol, "zbg").unwrap(); let (base, quote) = { let v: Vec<&str> = pair.split('/').collect(); (v[0].to_string(), v[1].to_string()) }; Market { exchange: "zbg".to_string(), market_type: MarketType::Spot, symbol: m.symbol, base_id: m.base_currency, quote_id: m.quote_currency, settle_id: None, base, quote, settle: None, active: m.state == "online", margin: false, // TODO: need to find zbg spot fees fees: Fees { maker: 0.002, taker: 0.002 }, precision: Precision { tick_size: 1.0 / (10_i64.pow(m.price_precision as u32) as f64), lot_size: 1.0 / (10_i64.pow(m.amount_precision as u32) as f64), }, quantity_limit: Some(QuantityLimit { min: m.min_order_amt.parse::().ok(), max: None, notional_min: None, notional_max: None, }), contract_value: None, delivery_date: None, info, } }) .collect(); Ok(markets) } ================================================ FILE: crypto-markets/src/exchanges/zbg/zbg_swap.rs ================================================ use std::collections::HashMap; use super::super::utils::http_get; use crate::{ error::{Error, Result}, Fees, Market, Precision, }; use crypto_market_type::MarketType; use serde::{Deserialize, Serialize}; use serde_json::Value; #[derive(Serialize, Deserialize)] #[allow(non_snake_case)] struct SwapMarket { symbol: String, currencyName: String, lotSize: String, contractId: i64, takerFeeRatio: String, commodityId: i64, currencyId: i64, contractUnit: String, makerFeeRatio: String, priceTick: String, commodityName: Option, #[serde(flatten)] extra: HashMap, } #[derive(Serialize, Deserialize)] struct ResMsg { message: String, method: Option, code: String, } #[derive(Serialize, Deserialize)] #[allow(non_snake_case)] struct Response { datas: Vec, resMsg: ResMsg, } // See https://zbgapi.github.io/docs/future/v1/en/#public-get-contracts fn fetch_swap_markets_raw() -> Result> { let txt = http_get("https://www.zbg.com/exchange/api/v1/future/common/contracts", None)?; let resp = serde_json::from_str::(&txt)?; if resp.resMsg.code != "1" { Err(Error(txt)) } else { Ok(resp.datas) } } pub(super) fn fetch_inverse_swap_symbols() -> Result> { let symbols = fetch_swap_markets_raw()? .into_iter() .map(|m| m.symbol) .filter(|x| x.ends_with("_USD-R")) .collect::>(); Ok(symbols) } pub(super) fn fetch_linear_swap_symbols() -> Result> { let symbols = fetch_swap_markets_raw()? .into_iter() .map(|m| m.symbol) .filter(|x| x.ends_with("_USDT") || x.ends_with("_ZUSD")) .collect::>(); Ok(symbols) } fn to_market(raw_market: &SwapMarket) -> Market { let pair = crypto_pair::normalize_pair(&raw_market.symbol, "zbg").unwrap(); let (base, quote) = { let v: Vec<&str> = pair.split('/').collect(); (v[0].to_string(), v[1].to_string()) }; let (base_id, quote_id) = { let v: Vec<&str> = raw_market.symbol.split('_').collect(); ( v[0].to_string(), if v[1].ends_with("-R") { v[1].strip_suffix("-R").unwrap().to_string() } else { v[1].to_string() }, ) }; let market_type = if raw_market.symbol.ends_with("_USD-R") { MarketType::InverseSwap } else if raw_market.symbol.ends_with("_USDT") { MarketType::LinearSwap } else { panic!( "Failed to detect market_type {}", serde_json::to_string_pretty(raw_market).unwrap() ); }; Market { exchange: "zbg".to_string(), market_type, symbol: raw_market.symbol.to_string(), base_id, quote_id, settle_id: Some(raw_market.currencyName.to_uppercase()), base, quote, settle: Some(raw_market.currencyName.to_uppercase()), active: true, margin: true, fees: Fees { maker: raw_market.makerFeeRatio.parse::().unwrap(), taker: raw_market.takerFeeRatio.parse::().unwrap(), }, precision: Precision { tick_size: raw_market.priceTick.parse::().unwrap(), lot_size: raw_market.lotSize.parse::().unwrap(), }, quantity_limit: None, contract_value: Some(raw_market.contractUnit.parse::().unwrap()), delivery_date: None, info: serde_json::to_value(raw_market).unwrap().as_object().unwrap().clone(), } } pub(super) fn fetch_inverse_swap_markets() -> Result> { let markets = fetch_swap_markets_raw()? .into_iter() .filter(|m| m.symbol.ends_with("_USD-R")) .map(|m| to_market(&m)) .collect::>(); Ok(markets) } pub(super) fn fetch_linear_swap_markets() -> Result> { let markets = fetch_swap_markets_raw()? .into_iter() .filter(|m| m.symbol.ends_with("_USDT")) .map(|m| to_market(&m)) .collect::>(); Ok(markets) } ================================================ FILE: crypto-markets/src/lib.rs ================================================ #![allow(clippy::unnecessary_wraps)] //! Get all trading pairs of a cryptocurrency exchange. //! //! ## Example //! //! ``` //! use crypto_markets::fetch_markets; //! use crypto_market_type::MarketType; //! //! let markets = fetch_markets("binance", MarketType::Spot).unwrap(); //! println!("{}", serde_json::to_string_pretty(&markets).unwrap()) //! ``` mod error; mod exchanges; mod market; use crypto_market_type::MarketType; pub use error::Error; pub use market::{Fees, Market, Precision, QuantityLimit}; use error::Result; /// Fetch trading symbols. pub fn fetch_symbols(exchange: &str, market_type: MarketType) -> Result> { match exchange { "binance" => exchanges::binance::fetch_symbols(market_type), "bitfinex" => exchanges::bitfinex::fetch_symbols(market_type), "bitget" => exchanges::bitget::fetch_symbols(market_type), "bithumb" => exchanges::bithumb::fetch_symbols(market_type), "bitmex" => exchanges::bitmex::fetch_symbols(market_type), "bitstamp" => exchanges::bitstamp::fetch_symbols(market_type), "bitz" => exchanges::bitz::fetch_symbols(market_type), "bybit" => exchanges::bybit::fetch_symbols(market_type), "coinbase_pro" => exchanges::coinbase_pro::fetch_symbols(market_type), "deribit" => exchanges::deribit::fetch_symbols(market_type), "dydx" => exchanges::dydx::fetch_symbols(market_type), "ftx" => exchanges::ftx::fetch_symbols(market_type), "gate" => exchanges::gate::fetch_symbols(market_type), "huobi" => exchanges::huobi::fetch_symbols(market_type), "kraken" => exchanges::kraken::fetch_symbols(market_type), "kucoin" => exchanges::kucoin::fetch_symbols(market_type), "mexc" => exchanges::mexc::fetch_symbols(market_type), "okx" => exchanges::okx::fetch_symbols(market_type), "zb" => exchanges::zb::fetch_symbols(market_type), "zbg" => exchanges::zbg::fetch_symbols(market_type), _ => panic!("Unsupported exchange {exchange}"), } } /// Fetch trading markets of a cryptocurrency exchange. /// /// # Arguments /// /// * `exchange` - The exchange name /// * `market_type` - The market type /// /// # Example /// /// ``` /// use crypto_markets::fetch_markets; /// use crypto_market_type::MarketType; /// let markets = fetch_markets("binance", MarketType::Spot).unwrap(); /// assert!(!markets.is_empty()); /// println!("{}", serde_json::to_string_pretty(&markets).unwrap()) /// ``` pub fn fetch_markets(exchange: &str, market_type: MarketType) -> Result> { match exchange { "binance" => exchanges::binance::fetch_markets(market_type), "bitfinex" => exchanges::bitfinex::fetch_markets(market_type), "bitget" => exchanges::bitget::fetch_markets(market_type), "bithumb" => exchanges::bithumb::fetch_markets(market_type), "bitmex" => exchanges::bitmex::fetch_markets(market_type), "bitstamp" => exchanges::bitstamp::fetch_markets(market_type), "bitz" => exchanges::bitz::fetch_markets(market_type), "bybit" => exchanges::bybit::fetch_markets(market_type), "coinbase_pro" => exchanges::coinbase_pro::fetch_markets(market_type), "deribit" => exchanges::deribit::fetch_markets(market_type), "dydx" => exchanges::dydx::fetch_markets(market_type), "ftx" => exchanges::ftx::fetch_markets(market_type), "gate" => exchanges::gate::fetch_markets(market_type), "huobi" => exchanges::huobi::fetch_markets(market_type), "kraken" => exchanges::kraken::fetch_markets(market_type), "kucoin" => exchanges::kucoin::fetch_markets(market_type), "mexc" => exchanges::mexc::fetch_markets(market_type), "okx" => exchanges::okx::fetch_markets(market_type), "zb" => exchanges::zb::fetch_markets(market_type), "zbg" => exchanges::zbg::fetch_markets(market_type), _ => panic!("Unsupported exchange {exchange}"), } } ================================================ FILE: crypto-markets/src/main.rs ================================================ use crypto_market_type::MarketType; use crypto_markets::fetch_markets; use std::{env, str::FromStr}; fn main() { let args: Vec = env::args().collect(); if args.len() != 3 { println!("Usage: crypto-markets "); return; } let exchange: &str = &args[1]; let market_type = MarketType::from_str(&args[2]); if market_type.is_err() { println!("Unknown market type: {}", &args[2]); return; } let resp = fetch_markets(exchange, market_type.unwrap()); match resp { Ok(markets) => println!("{}", serde_json::to_string_pretty(&markets).unwrap()), Err(err) => println!("{err}"), } } ================================================ FILE: crypto-markets/src/market.rs ================================================ use crypto_market_type::MarketType; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; #[derive(Clone, Serialize, Deserialize)] pub struct Fees { pub maker: f64, pub taker: f64, } #[derive(Clone, Serialize, Deserialize)] pub struct Precision { /// the minimum price change, see https://en.wikipedia.org/wiki/Tick_size pub tick_size: f64, /// the minimum quantity change pub lot_size: f64, } #[derive(Clone, Serialize, Deserialize, PartialEq)] pub struct QuantityLimit { /// Minimum base quantity #[serde(skip_serializing_if = "Option::is_none")] pub min: Option, /// Maximum base quantity #[serde(skip_serializing_if = "Option::is_none")] pub max: Option, /// Notional minimum size #[serde(skip_serializing_if = "Option::is_none")] pub notional_min: Option, /// Notional maximum size #[serde(skip_serializing_if = "Option::is_none")] pub notional_max: Option, } /// Market contains all information about a market #[derive(Clone, Serialize, Deserialize)] pub struct Market { /// exchange name pub exchange: String, /// Market type pub market_type: MarketType, /// exchange-specific trading symbol, recognized by RESTful API, equivalent /// to ccxt's Market.id. pub symbol: String, /// exchange-specific base currency pub base_id: String, /// exchange-specific quote currency pub quote_id: String, /// exchange-specific settlement currency, i.e., collateral currency, always /// None for spot markets #[serde(skip_serializing_if = "Option::is_none")] pub settle_id: Option, /// unified uppercase string of base fiat or crypto currency pub base: String, /// unified uppercase string of quote fiat or crypto currency pub quote: String, /// settlement currency, i.e., collateral currency, always None for spot /// markets #[serde(skip_serializing_if = "Option::is_none")] pub settle: Option, /// market status pub active: bool, /// Margin enabled. /// /// * All contract markets are margin enabled, including future, swap and /// option. /// * Only a few exchanges have spot market with margin enabled. pub margin: bool, pub fees: Fees, /// number of decimal digits after the dot pub precision: Precision, /// the min and max values of quantity #[serde(skip_serializing_if = "Option::is_none")] pub quantity_limit: Option, // The value of one contract, not applicable to sport markets #[serde(skip_serializing_if = "Option::is_none")] pub contract_value: Option, /// Delivery date, unix timestamp in milliseconds, only applicable for /// future and option markets. #[serde(skip_serializing_if = "Option::is_none")] pub delivery_date: Option, /// the original JSON string retrieved from the exchange pub info: Map, } ================================================ FILE: crypto-markets/tests/binance.rs ================================================ use crypto_market_type::{get_market_types, MarketType}; use crypto_markets::{fetch_markets, fetch_symbols}; use crypto_pair::get_market_type; use test_case::test_case; #[macro_use] mod utils; const EXCHANGE_NAME: &str = "binance"; #[test] fn fetch_all_symbols() { gen_all_symbols!(); } #[test] fn fetch_spot_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::Spot).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { assert_eq!(symbol.to_uppercase(), symbol.to_string()); assert_eq!(MarketType::Spot, get_market_type(symbol, EXCHANGE_NAME, Some(true))); } } #[test] fn fetch_inverse_future_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::InverseFuture).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { let date = &symbol[(symbol.len() - 6)..]; assert!(date.parse::().is_ok()); let quote = &symbol[(symbol.len() - 10)..(symbol.len() - 7)]; assert_eq!(quote, "USD"); assert_eq!(MarketType::InverseFuture, get_market_type(symbol, EXCHANGE_NAME, None)); } } #[test] fn fetch_linear_future_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::LinearFuture).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { let date = &symbol[(symbol.len() - 6)..]; assert!(date.parse::().is_ok()); let quote = &symbol[(symbol.len() - 11)..(symbol.len() - 7)]; assert_eq!(quote, "USDT"); assert_eq!(MarketType::LinearFuture, get_market_type(symbol, EXCHANGE_NAME, None)); } } #[test] fn fetch_inverse_swap_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::InverseSwap).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { assert!(symbol.ends_with("USD_PERP")); assert_eq!(MarketType::InverseSwap, get_market_type(symbol, EXCHANGE_NAME, None)); } } #[test] fn fetch_linear_swap_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::LinearSwap).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { assert!(symbol.ends_with("USDT") || symbol.ends_with("BUSD")); assert_eq!(MarketType::LinearSwap, get_market_type(symbol, EXCHANGE_NAME, None)); } } #[ignore] #[test] fn fetch_option_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::EuropeanOption).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { assert!(symbol.ends_with("-P") || symbol.ends_with("-C")); assert_eq!(MarketType::EuropeanOption, get_market_type(symbol, EXCHANGE_NAME, None)); } } #[test] fn fetch_spot_markets() { let markets = fetch_markets(EXCHANGE_NAME, MarketType::Spot).unwrap(); assert!(!markets.is_empty()); let btc_usdt = markets.iter().find(|m| m.symbol == "BTCUSDT").unwrap().clone(); assert!(btc_usdt.contract_value.is_none()); assert_eq!(btc_usdt.precision.tick_size, 0.01); assert_eq!(btc_usdt.precision.lot_size, 0.00001); let quantity_limit = btc_usdt.quantity_limit.unwrap(); assert_eq!(quantity_limit.min.unwrap(), 0.00001); assert_eq!(quantity_limit.max, Some(9000.0)); } #[test] fn fetch_inverse_future_markets() { let markets = fetch_markets(EXCHANGE_NAME, MarketType::InverseFuture).unwrap(); assert!(!markets.is_empty()); let btcusd = markets.iter().find(|m| m.symbol.starts_with("BTCUSD_")).unwrap().clone(); assert_eq!(btcusd.contract_value, Some(100.0)); assert_eq!(btcusd.precision.tick_size, 0.1); assert_eq!(btcusd.precision.lot_size, 1.0); let quantity_limit = btcusd.quantity_limit.unwrap(); assert_eq!(quantity_limit.min.unwrap(), 1.0); assert_eq!(quantity_limit.max, Some(1000000.0)); } #[test] fn fetch_inverse_swap_markets() { let markets = fetch_markets(EXCHANGE_NAME, MarketType::InverseSwap).unwrap(); assert!(!markets.is_empty()); let btcusd_perp = markets.iter().find(|m| m.symbol == "BTCUSD_PERP").unwrap().clone(); assert_eq!(btcusd_perp.contract_value, Some(100.0)); assert_eq!(btcusd_perp.precision.tick_size, 0.1); assert_eq!(btcusd_perp.precision.lot_size, 1.0); let quantity_limit = btcusd_perp.quantity_limit.unwrap(); assert_eq!(quantity_limit.min.unwrap(), 1.0); assert_eq!(quantity_limit.max, Some(1000000.0)); } #[test] fn fetch_linear_swap_markets() { let markets = fetch_markets(EXCHANGE_NAME, MarketType::LinearSwap).unwrap(); assert!(!markets.is_empty()); let btcusdt = markets.iter().find(|m| m.symbol == "BTCUSDT").unwrap().clone(); assert_eq!(btcusdt.contract_value, Some(1.0)); assert_eq!(btcusdt.precision.tick_size, 0.01); assert_eq!(btcusdt.precision.lot_size, 0.001); let quantity_limit = btcusdt.quantity_limit.unwrap(); assert_eq!(quantity_limit.min.unwrap(), 0.001); assert_eq!(quantity_limit.max, Some(1000.0)); } #[ignore] #[test] fn fetch_option_markets() { let markets = fetch_markets(EXCHANGE_NAME, MarketType::EuropeanOption).unwrap(); assert!(!markets.is_empty()); let btcusd = markets.iter().find(|m| m.symbol.starts_with("BTC-")).unwrap().clone(); assert_eq!(btcusd.contract_value, Some(1.0)); assert_eq!(btcusd.precision.tick_size, 0.01); assert_eq!(btcusd.precision.lot_size, 0.0001); let quantity_limit = btcusd.quantity_limit.unwrap(); assert_eq!(quantity_limit.min.unwrap(), 0.0002); assert_eq!(quantity_limit.max, Some(10000.0)); } #[test_case(MarketType::InverseFuture)] #[test_case(MarketType::LinearFuture)] #[test_case(MarketType::InverseSwap)] #[test_case(MarketType::LinearSwap)] // #[test_case(MarketType::EuropeanOption)] fn test_contract_values(market_type: MarketType) { check_contract_values!(EXCHANGE_NAME, market_type); } ================================================ FILE: crypto-markets/tests/bitfinex.rs ================================================ use crypto_market_type::{get_market_types, MarketType}; use crypto_markets::{fetch_markets, fetch_symbols}; use crypto_pair::get_market_type; use test_case::test_case; #[macro_use] mod utils; const EXCHANGE_NAME: &str = "bitfinex"; #[test] fn fetch_all_symbols() { gen_all_symbols!(); } #[test] fn fetch_spot_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::Spot).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { assert!(symbol.starts_with('t')); assert_eq!(MarketType::Spot, get_market_type(symbol, EXCHANGE_NAME, None)); } } #[test] fn fetch_linear_swap_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::LinearSwap).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { assert!(symbol.starts_with('t')); assert!( symbol.ends_with("F0:USTF0") || symbol.ends_with("F0:BTCF0") || symbol.ends_with("F0:EUTF0") ); assert_eq!(MarketType::LinearSwap, get_market_type(symbol, EXCHANGE_NAME, None)); } } #[test] fn fetch_spot_markets() { let markets = fetch_markets(EXCHANGE_NAME, MarketType::Spot).unwrap(); assert!(!markets.is_empty()); let btc_usdt = markets.iter().find(|m| m.symbol == "tBTCUST").unwrap().clone(); assert_eq!(btc_usdt.precision.tick_size, 0.00001); assert_eq!(btc_usdt.precision.lot_size, 0.00000001); let quantity_limit = btc_usdt.quantity_limit.unwrap(); assert_eq!(quantity_limit.min.unwrap(), 0.00006); assert_eq!(quantity_limit.max, Some(2000.0)); } #[test] fn fetch_linear_swap_markets() { let markets = fetch_markets(EXCHANGE_NAME, MarketType::LinearSwap).unwrap(); assert!(!markets.is_empty()); let btc_usdt = markets.iter().find(|m| m.symbol == "tBTCF0:USTF0").unwrap().clone(); assert_eq!(btc_usdt.precision.tick_size, 0.00001); assert_eq!(btc_usdt.precision.lot_size, 0.00000001); let quantity_limit = btc_usdt.quantity_limit.unwrap(); assert_eq!(quantity_limit.min.unwrap(), 0.00006); assert_eq!(quantity_limit.max, Some(100.0)); } #[test_case(MarketType::LinearSwap)] fn test_contract_values(market_type: MarketType) { check_contract_values!(EXCHANGE_NAME, market_type); } ================================================ FILE: crypto-markets/tests/bitget.rs ================================================ use crypto_market_type::{get_market_types, MarketType}; use crypto_markets::{fetch_markets, fetch_symbols}; use crypto_pair::get_market_type; // use test_case::test_case; #[macro_use] mod utils; const EXCHANGE_NAME: &str = "bitget"; #[test] fn fetch_all_symbols() { gen_all_symbols!(); } #[test] fn fetch_spot_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::Spot).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { assert!( symbol.ends_with("USDT_SPBL") || symbol.ends_with("BTC_SPBL") || symbol.ends_with("ETH_SPBL") ); assert_eq!(MarketType::Spot, get_market_type(symbol, EXCHANGE_NAME, None)); } } #[test] fn fetch_inverse_swap_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::InverseSwap).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { assert!(symbol.ends_with("USD_DMCBL")); assert_eq!(MarketType::InverseSwap, get_market_type(symbol, EXCHANGE_NAME, None)); } } #[test] fn fetch_linear_swap_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::LinearSwap).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { assert!(symbol.ends_with("USDT_UMCBL") || symbol.ends_with("PERP_CMCBL")); assert_eq!(MarketType::LinearSwap, get_market_type(symbol, EXCHANGE_NAME, None)); } } #[test] fn fetch_inverse_future_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::InverseFuture).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { println!("{symbol}"); assert!(symbol.contains("USD_DMCBL_")); assert_eq!(MarketType::InverseFuture, get_market_type(symbol, EXCHANGE_NAME, None)); } } #[test] fn fetch_spot_markets() { let markets = fetch_markets(EXCHANGE_NAME, MarketType::Spot).unwrap(); assert!(!markets.is_empty()); let btcusdt = markets.iter().find(|m| m.symbol == "BTCUSDT_SPBL").unwrap().clone(); assert_eq!(btcusdt.precision.tick_size, 0.01); assert_eq!(btcusdt.precision.lot_size, 0.0001); let quantity_limit = btcusdt.quantity_limit.unwrap(); assert_eq!(0.0001, quantity_limit.min.unwrap()); assert!(quantity_limit.max.is_none()); } #[test] fn fetch_inverse_swap_markets() { let markets = fetch_markets(EXCHANGE_NAME, MarketType::InverseSwap).unwrap(); assert!(!markets.is_empty()); let btcusd = markets.iter().find(|m| m.symbol == "BTCUSD_DMCBL").unwrap().clone(); assert_eq!(btcusd.contract_value, Some(1.0)); assert_eq!(btcusd.precision.tick_size, 0.1); assert_eq!(btcusd.precision.lot_size, 0.001); let quantity_limit = btcusd.quantity_limit.unwrap(); assert_eq!(0.001, quantity_limit.min.unwrap()); assert!(quantity_limit.max.is_none()); } #[test] fn fetch_linear_swap_markets() { let markets = fetch_markets(EXCHANGE_NAME, MarketType::LinearSwap).unwrap(); assert!(!markets.is_empty()); let btcusdt = markets.iter().find(|m| m.symbol == "BTCUSDT_UMCBL").unwrap().clone(); assert_eq!(btcusdt.contract_value, Some(1.0)); assert_eq!(btcusdt.precision.tick_size, 0.1); assert_eq!(btcusdt.precision.lot_size, 0.001); let quantity_limit = btcusdt.quantity_limit.unwrap(); assert_eq!(0.001, quantity_limit.min.unwrap()); assert!(quantity_limit.max.is_none()); } #[test] fn fetch_inverse_future_markets() { let markets = fetch_markets(EXCHANGE_NAME, MarketType::InverseFuture).unwrap(); assert!(!markets.is_empty()); let btcusd = markets.iter().find(|m| m.symbol.contains("BTCUSD_DMCBL_")).unwrap().clone(); assert_eq!(btcusd.contract_value, Some(1.0)); assert_eq!(btcusd.precision.tick_size, 0.1); assert_eq!(btcusd.precision.lot_size, 0.001); assert!(btcusd.delivery_date.is_some()); let quantity_limit = btcusd.quantity_limit.unwrap(); assert_eq!(0.001, quantity_limit.min.unwrap()); assert!(quantity_limit.max.is_none()); } // #[test_case(MarketType::InverseSwap)] // #[test_case(MarketType::LinearSwap)] // fn test_contract_values(market_type: MarketType) { // check_contract_values!(EXCHANGE_NAME, market_type); // } ================================================ FILE: crypto-markets/tests/bithumb.rs ================================================ use crypto_market_type::{get_market_types, MarketType}; use crypto_markets::{fetch_markets, fetch_symbols}; use crypto_pair::get_market_type; #[macro_use] mod utils; const EXCHANGE_NAME: &str = "bithumb"; #[test] fn fetch_all_symbols() { gen_all_symbols!(); } #[test] fn fetch_spot_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::Spot).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { assert!(symbol.contains('-')); assert_eq!(symbol.to_string(), symbol.to_uppercase()); assert_eq!(MarketType::Spot, get_market_type(symbol, EXCHANGE_NAME, None)); } } #[test] fn fetch_spot_markets() { let markets = fetch_markets(EXCHANGE_NAME, MarketType::Spot).unwrap(); assert!(!markets.is_empty()); let btc_usdt = markets.iter().find(|m| m.symbol == "BTC-USDT").unwrap().clone(); assert_eq!(btc_usdt.precision.tick_size, 0.01); assert_eq!(btc_usdt.precision.lot_size, 0.000001); assert!(btc_usdt.quantity_limit.is_none()); } ================================================ FILE: crypto-markets/tests/bitmex.rs ================================================ use crypto_market_type::MarketType; use crypto_markets::{fetch_markets, fetch_symbols}; use crypto_pair::get_market_type; #[macro_use] mod utils; const EXCHANGE_NAME: &str = "bitmex"; #[test] fn fetch_all_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::Unknown).unwrap(); assert!(!symbols.is_empty()); } #[test] #[ignore = "to avoid 429 Too Many Requests"] fn fetch_spot_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::Spot).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { assert!(symbol.ends_with("_USDT")); assert_eq!(MarketType::Spot, get_market_type(symbol, EXCHANGE_NAME, None)); } } #[test] #[ignore = "to avoid 429 Too Many Requests"] fn fetch_inverse_swap_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::InverseSwap).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { assert!(symbol.starts_with("XBT") || symbol.starts_with("ETH")); assert!(symbol.ends_with("USD") || symbol.ends_with("EUR") || symbol.ends_with("_ETH")); assert_eq!(MarketType::InverseSwap, get_market_type(symbol, EXCHANGE_NAME, None)); } } #[test] #[ignore = "to avoid 429 Too Many Requests"] fn fetch_linear_swap_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::LinearSwap).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { assert!(symbol.ends_with("USDT")); assert_eq!(MarketType::LinearSwap, get_market_type(symbol, EXCHANGE_NAME, None)); } } #[test] #[ignore = "to avoid 429 Too Many Requests"] fn fetch_quanto_swap_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::QuantoSwap).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { assert!(symbol.ends_with("USD") || symbol.ends_with("USDT")); assert_eq!(MarketType::QuantoSwap, get_market_type(symbol, EXCHANGE_NAME, None)); } } #[test] #[ignore = "to avoid 429 Too Many Requests"] fn fetch_inverse_future_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::InverseFuture).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { assert!(symbol.starts_with("XBT") || symbol.starts_with("ETH")); let date = if let Some(pos) = symbol.rfind('_') { // e.g., ETHUSDM22_ETH &symbol[(pos - 2)..pos] } else { &symbol[(symbol.len() - 2)..] }; assert!(date.parse::().is_ok()); assert_eq!(MarketType::InverseFuture, get_market_type(symbol, EXCHANGE_NAME, None)); } } #[test] #[ignore = "to avoid 429 Too Many Requests"] fn fetch_quanto_future_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::QuantoFuture).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { assert!(!symbol.starts_with("XBT")); let date = &symbol[(symbol.len() - 2)..]; assert!(date.parse::().is_ok()); let quote = if symbol.chars().nth(symbol.len() - 4).unwrap() == 'T' { &symbol[(symbol.len() - 7)..(symbol.len() - 3)] } else { &symbol[(symbol.len() - 6)..(symbol.len() - 3)] }; assert!(quote == "USD" || quote == "USDT"); assert_eq!(MarketType::QuantoFuture, get_market_type(symbol, EXCHANGE_NAME, None)); } } #[test] #[ignore = "to avoid 429 Too Many Requests"] fn fetch_linear_future_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::LinearFuture).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { let date = &symbol[(symbol.len() - 2)..]; assert!(date.parse::().is_ok()); assert_eq!(MarketType::LinearFuture, get_market_type(symbol, EXCHANGE_NAME, None)); } } #[test] fn fetch_inverse_swap_markets() { let markets = fetch_markets(EXCHANGE_NAME, MarketType::InverseSwap).unwrap(); assert!(!markets.is_empty()); let xbtusd = markets.iter().find(|m| m.symbol == "XBTUSD").unwrap(); assert_eq!(xbtusd.precision.tick_size, 0.5); assert_eq!(xbtusd.precision.lot_size, 100.0); assert_eq!(xbtusd.contract_value, Some(1.0)); assert!(xbtusd.quantity_limit.is_none()); } #[test] fn test_contract_values() { check_contract_values!(EXCHANGE_NAME, MarketType::Unknown); } ================================================ FILE: crypto-markets/tests/bitstamp.rs ================================================ use crypto_market_type::{get_market_types, MarketType}; use crypto_markets::{fetch_markets, fetch_symbols}; use crypto_pair::get_market_type; #[macro_use] mod utils; const EXCHANGE_NAME: &str = "bitstamp"; #[test] fn fetch_all_symbols() { gen_all_symbols!(); } #[test] fn fetch_spot_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::Spot).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { assert_eq!(MarketType::Spot, get_market_type(symbol, EXCHANGE_NAME, None)); } } #[test] fn fetch_spot_markets() { let markets = fetch_markets(EXCHANGE_NAME, MarketType::Spot).unwrap(); assert!(!markets.is_empty()); let btcusd = markets.iter().find(|m| m.symbol == "btcusd").unwrap().clone(); assert_eq!(btcusd.precision.tick_size, 0.00000001); assert_eq!(btcusd.precision.lot_size, 1.0); assert!(btcusd.quantity_limit.is_none()); } ================================================ FILE: crypto-markets/tests/bitz.rs ================================================ use crypto_market_type::{get_market_types, MarketType}; use crypto_markets::fetch_symbols; #[macro_use] mod utils; const EXCHANGE_NAME: &str = "bitz"; #[test] #[ignore = "bitz.com has shutdown since October 2021"] fn fetch_all_symbols() { gen_all_symbols!(); } #[test] #[ignore = "bitz.com has shutdown since October 2021"] fn fetch_spot_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::Spot).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { assert!(symbol.contains('_')); } } #[test] #[ignore = "bitz.com has shutdown since October 2021"] fn fetch_inverse_swap_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::InverseSwap).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { assert!(symbol.ends_with("_USD")); } } #[test] #[ignore = "bitz.com has shutdown since October 2021"] fn fetch_linear_swap_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::LinearSwap).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { assert!(symbol.ends_with("_USDT")); } } ================================================ FILE: crypto-markets/tests/bybit.rs ================================================ use crypto_market_type::{get_market_types, MarketType}; use crypto_markets::{fetch_markets, fetch_symbols}; use crypto_pair::get_market_type; use test_case::test_case; #[macro_use] mod utils; const EXCHANGE_NAME: &str = "bybit"; #[test] fn fetch_all_symbols() { gen_all_symbols!(); } #[test] fn fetch_inverse_swap_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::InverseSwap).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { assert!(symbol.ends_with("USD")); assert_eq!(MarketType::InverseSwap, get_market_type(symbol, EXCHANGE_NAME, None)); } } #[test] fn fetch_linear_swap_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::LinearSwap).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { assert!(symbol.ends_with("USDT")); assert_eq!(MarketType::LinearSwap, get_market_type(symbol, EXCHANGE_NAME, None)); } } #[test] fn fetch_inverse_future_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::InverseFuture).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { let date = &symbol[(symbol.len() - 2)..]; assert!(date.parse::().is_ok()); assert_eq!(MarketType::InverseFuture, get_market_type(symbol, EXCHANGE_NAME, None)); } } #[test] fn fetch_inverse_swap_markets() { let markets = fetch_markets(EXCHANGE_NAME, MarketType::InverseSwap).unwrap(); assert!(!markets.is_empty()); for market in markets.iter() { assert!(market.delivery_date.is_none()); } let btcusd = markets.iter().find(|m| m.symbol == "BTCUSD").unwrap().clone(); assert_eq!(btcusd.precision.tick_size, 0.5); assert_eq!(btcusd.precision.lot_size, 1.0); let quantity_limit = btcusd.quantity_limit.unwrap(); assert_eq!(quantity_limit.min.unwrap(), 1.0); assert_eq!(quantity_limit.max, Some(1000000.0)); } #[test] fn fetch_linear_swap_markets() { let markets = fetch_markets(EXCHANGE_NAME, MarketType::LinearSwap).unwrap(); assert!(!markets.is_empty()); for market in markets.iter() { assert!(market.delivery_date.is_none()); } let btcusdt = markets.iter().find(|m| m.symbol == "BTCUSDT").unwrap().clone(); assert_eq!(btcusdt.precision.tick_size, 0.1); assert_eq!(btcusdt.precision.lot_size, 0.001); let quantity_limit = btcusdt.quantity_limit.unwrap(); assert_eq!(quantity_limit.min.unwrap(), 0.001); assert_eq!(quantity_limit.max, Some(100.0)); } #[test] fn fetch_inverse_future_markets() { let markets = fetch_markets(EXCHANGE_NAME, MarketType::InverseFuture).unwrap(); assert!(!markets.is_empty()); for market in markets.iter() { assert!(market.delivery_date.is_some()); } let btcusd = markets.iter().find(|m| m.symbol.starts_with("BTCUSD")).unwrap().clone(); assert_eq!(btcusd.precision.tick_size, 0.5); assert_eq!(btcusd.precision.lot_size, 1.0); let quantity_limit = btcusd.quantity_limit.unwrap(); assert_eq!(quantity_limit.min.unwrap(), 1.0); assert_eq!(quantity_limit.max, Some(1000000.0)); } #[test_case(MarketType::InverseFuture)] #[test_case(MarketType::InverseSwap)] #[test_case(MarketType::LinearSwap)] fn test_contract_values(market_type: MarketType) { check_contract_values!(EXCHANGE_NAME, market_type); } ================================================ FILE: crypto-markets/tests/coinbase_pro.rs ================================================ use crypto_market_type::{get_market_types, MarketType}; use crypto_markets::{fetch_markets, fetch_symbols}; use crypto_pair::get_market_type; #[macro_use] mod utils; const EXCHANGE_NAME: &str = "coinbase_pro"; #[test] fn fetch_all_symbols() { gen_all_symbols!(); } #[test] fn fetch_spot_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::Spot).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { assert!(symbol.contains('-')); assert_eq!(symbol.to_string(), symbol.to_uppercase()); assert_eq!(MarketType::Spot, get_market_type(symbol, EXCHANGE_NAME, None)); } } #[test] fn fetch_spot_markets() { let markets = fetch_markets(EXCHANGE_NAME, MarketType::Spot).unwrap(); assert!(!markets.is_empty()); let btcusd = markets.iter().find(|m| m.symbol == "BTC-USD").unwrap().clone(); assert_eq!(btcusd.precision.tick_size, 0.01); assert_eq!(btcusd.precision.lot_size, 0.00000001); let quantity_limit = btcusd.quantity_limit.unwrap(); assert_eq!(quantity_limit.min, None); assert_eq!(quantity_limit.max, None); assert_eq!(1.0, quantity_limit.notional_min.unwrap()); } ================================================ FILE: crypto-markets/tests/deribit.rs ================================================ use crypto_market_type::MarketType; use crypto_markets::{fetch_markets, fetch_symbols}; use crypto_pair::get_market_type; use test_case::test_case; #[macro_use] mod utils; const EXCHANGE_NAME: &str = "deribit"; #[test] fn fetch_inverse_future_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::InverseFuture).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { let year = &symbol[(symbol.len() - 2)..]; assert!(year.parse::().is_ok()); let date = &symbol[(symbol.len() - 7)..(symbol.len() - 5)]; assert!(date.parse::().is_ok()); assert_eq!(MarketType::InverseFuture, get_market_type(symbol, EXCHANGE_NAME, None)); } } #[test] fn fetch_inverse_swap_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::InverseSwap).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { assert!(symbol.ends_with("-PERPETUAL")); assert_eq!(MarketType::InverseSwap, get_market_type(symbol, EXCHANGE_NAME, None)); } } #[test] fn fetch_option_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::EuropeanOption).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { let arr: Vec<&str> = symbol.split('-').collect(); assert!(arr[0] == "BTC" || arr[0] == "ETH"); let date = &arr[1][..(arr[1].len() - 5)]; assert!(date.parse::().is_ok()); let year = &arr[1][(arr[1].len() - 2)..]; assert!(year.parse::().is_ok()); assert!(arr[2].parse::().is_ok()); assert!(arr[3] == "C" || arr[3] == "P"); assert_eq!(MarketType::EuropeanOption, get_market_type(symbol, EXCHANGE_NAME, None)); } } #[test] fn fetch_inverse_future_markets() { let markets = fetch_markets(EXCHANGE_NAME, MarketType::InverseFuture).unwrap(); assert!(!markets.is_empty()); let btcusd = markets.iter().find(|m| m.base_id == "BTC").unwrap().clone(); assert_eq!(btcusd.contract_value, Some(10.0)); assert_eq!(btcusd.precision.tick_size, 2.5); assert_eq!(btcusd.precision.lot_size, 10.0); let quantity_limit = btcusd.quantity_limit.unwrap(); assert_eq!(quantity_limit.min.unwrap(), 10.0); assert_eq!(quantity_limit.max, None); } #[test] fn fetch_inverse_swap_markets() { let markets = fetch_markets(EXCHANGE_NAME, MarketType::InverseSwap).unwrap(); assert!(!markets.is_empty()); let btcusd = markets.iter().find(|m| m.base_id == "BTC").unwrap().clone(); assert_eq!(btcusd.contract_value, Some(10.0)); assert_eq!(btcusd.precision.tick_size, 0.5); assert_eq!(btcusd.precision.lot_size, 10.0); let quantity_limit = btcusd.quantity_limit.unwrap(); assert_eq!(quantity_limit.min.unwrap(), 10.0); assert_eq!(quantity_limit.max, None); } #[test] fn fetch_option_markets() { let markets = fetch_markets(EXCHANGE_NAME, MarketType::EuropeanOption).unwrap(); assert!(!markets.is_empty()); let btcusd = markets.iter().find(|m| m.base_id == "BTC").unwrap().clone(); assert_eq!(btcusd.contract_value, Some(1.0)); assert_eq!(btcusd.precision.tick_size, 0.0005); assert_eq!(btcusd.precision.lot_size, 0.1); let quantity_limit = btcusd.quantity_limit.unwrap(); assert_eq!(quantity_limit.min.unwrap(), 0.1); assert_eq!(quantity_limit.max, None); } #[test_case(MarketType::InverseFuture)] #[test_case(MarketType::InverseSwap)] #[test_case(MarketType::EuropeanOption)] fn test_contract_values(market_type: MarketType) { check_contract_values!(EXCHANGE_NAME, market_type); } ================================================ FILE: crypto-markets/tests/dydx.rs ================================================ use crypto_market_type::{get_market_types, MarketType}; use crypto_markets::{fetch_markets, fetch_symbols}; use crypto_pair::get_market_type; use test_case::test_case; #[macro_use] mod utils; const EXCHANGE_NAME: &str = "dydx"; #[test] fn fetch_all_symbols() { gen_all_symbols!(); } #[test] fn fetch_linear_swap_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::LinearSwap).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { assert!(symbol.ends_with("-USD")); assert_eq!(MarketType::LinearSwap, get_market_type(symbol, EXCHANGE_NAME, None)); } } #[test] fn fetch_linear_swap_markets() { let markets = fetch_markets(EXCHANGE_NAME, MarketType::LinearSwap).unwrap(); assert!(!markets.is_empty()); let btcusd = markets.iter().find(|m| m.symbol == "BTC-USD").unwrap().clone(); assert_eq!(btcusd.precision.tick_size, 1.0); assert_eq!(btcusd.precision.lot_size, 0.0001); let quantity_limit = btcusd.quantity_limit.unwrap(); assert_eq!(quantity_limit.min.unwrap(), 0.001); assert_eq!(quantity_limit.max, None); } #[test_case(MarketType::LinearSwap)] fn test_contract_values(market_type: MarketType) { check_contract_values!(EXCHANGE_NAME, market_type); } ================================================ FILE: crypto-markets/tests/ftx.rs ================================================ use crypto_market_type::{get_market_types, MarketType}; use crypto_markets::{fetch_markets, fetch_symbols}; use crypto_pair::get_market_type; #[macro_use] mod utils; const EXCHANGE_NAME: &str = "ftx"; #[ignore = "The FTX website is not operational."] #[test] fn fetch_all_symbols() { gen_all_symbols!(); } #[ignore = "The FTX website is not operational."] #[test] fn fetch_spot_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::Spot).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { assert!(symbol.contains('/')); assert_eq!(*symbol, symbol.to_uppercase()); assert_eq!(MarketType::Spot, get_market_type(symbol, EXCHANGE_NAME, None)); } } #[ignore = "The FTX website is not operational."] #[test] fn fetch_linear_swap_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::LinearSwap).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { assert!(symbol.ends_with("-PERP")); assert_eq!(MarketType::LinearSwap, get_market_type(symbol, EXCHANGE_NAME, None)); } } #[ignore = "The FTX website is not operational."] #[test] fn fetch_linear_future_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::LinearFuture).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { let date = &symbol[(symbol.len() - 4)..]; assert!(date.parse::().is_ok()); assert_eq!(MarketType::LinearFuture, get_market_type(symbol, EXCHANGE_NAME, None)); } } #[ignore = "The FTX website is not operational."] #[test] fn fetch_move_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::Move).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { assert!(symbol.contains("-MOVE-")); assert_eq!(MarketType::Move, get_market_type(symbol, EXCHANGE_NAME, None)); } } #[ignore = "The FTX website is not operational."] #[test] fn fetch_bvol_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::BVOL).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { assert!(symbol.contains("BVOL/")); assert_eq!(MarketType::BVOL, get_market_type(symbol, EXCHANGE_NAME, None)); } } #[ignore = "The FTX website is not operational."] #[test] fn fetch_spot_markets() { let markets = fetch_markets(EXCHANGE_NAME, MarketType::Spot).unwrap(); assert!(!markets.is_empty()); let btcusdt = markets.iter().find(|m| m.symbol == "BTC/USDT").unwrap().clone(); assert!(btcusdt.contract_value.is_none()); assert_eq!(btcusdt.precision.tick_size, 1.0); assert_eq!(btcusdt.precision.lot_size, 0.0001); assert!(btcusdt.quantity_limit.is_none()); } #[ignore = "The FTX website is not operational."] #[test] fn fetch_linear_swap_markets() { let markets = fetch_markets(EXCHANGE_NAME, MarketType::LinearSwap).unwrap(); assert!(!markets.is_empty()); let btcusd = markets.iter().find(|m| m.symbol == "BTC-PERP").unwrap().clone(); assert_eq!(btcusd.contract_value, Some(1.0)); assert_eq!(btcusd.precision.tick_size, 1.0); assert_eq!(btcusd.precision.lot_size, 0.0001); assert!(btcusd.quantity_limit.is_none()); } #[ignore = "The FTX website is not operational."] #[test] fn fetch_linear_future_markets() { let markets = fetch_markets(EXCHANGE_NAME, MarketType::LinearFuture).unwrap(); assert!(!markets.is_empty()); let btcusd = markets.iter().find(|m| m.symbol.starts_with("BTC-")).unwrap().clone(); assert_eq!(btcusd.contract_value, Some(1.0)); assert_eq!(btcusd.precision.tick_size, 1.0); assert_eq!(btcusd.precision.lot_size, 0.0001); assert!(btcusd.quantity_limit.is_none()); assert!(btcusd.delivery_date.is_some()); } #[ignore = "The FTX website is not operational."] #[test] fn fetch_move_markets() { let markets = fetch_markets(EXCHANGE_NAME, MarketType::Move).unwrap(); assert!(!markets.is_empty()); let btcusd = markets.iter().find(|m| m.symbol.starts_with("BTC-MOVE-")).unwrap().clone(); assert_eq!(btcusd.contract_value, Some(1.0)); assert_eq!(btcusd.precision.tick_size, 1.0); assert_eq!(btcusd.precision.lot_size, 0.0001); assert!(btcusd.quantity_limit.is_none()); assert!(btcusd.delivery_date.is_some()); } #[ignore = "The FTX website is not operational."] #[test] fn fetch_bvol_markets() { let markets = fetch_markets(EXCHANGE_NAME, MarketType::BVOL).unwrap(); assert!(!markets.is_empty()); let btcusd = markets.iter().find(|m| m.symbol == "BVOL/USD").unwrap().clone(); assert_eq!(btcusd.contract_value, None); assert_eq!(btcusd.precision.tick_size, 0.025); assert_eq!(btcusd.precision.lot_size, 0.001); assert!(btcusd.quantity_limit.is_none()); assert!(btcusd.delivery_date.is_none()); } // The FTX website is not operational // #[test_case(MarketType::LinearSwap)] // #[test_case(MarketType::LinearFuture)] // fn test_contract_values(market_type: MarketType) { // check_contract_values!(EXCHANGE_NAME, market_type); // } ================================================ FILE: crypto-markets/tests/gate.rs ================================================ use crypto_market_type::{get_market_types, MarketType}; use crypto_markets::{fetch_markets, fetch_symbols}; use test_case::test_case; #[macro_use] mod utils; const EXCHANGE_NAME: &str = "gate"; #[test] fn fetch_all_symbols() { gen_all_symbols!(); } #[test] fn fetch_spot_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::Spot).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { if symbol.ends_with("_USD") { println!("{symbol}"); } assert!(symbol.contains('_')); assert_eq!(symbol.to_string(), symbol.to_uppercase()); } } #[test] fn fetch_inverse_swap_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::InverseSwap).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { assert!(symbol.ends_with("_USD")); } } #[test] fn fetch_linear_swap_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::LinearSwap).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { assert!(symbol.ends_with("_USDT")); } } #[test] fn fetch_inverse_future_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::InverseFuture).unwrap(); for symbol in symbols.iter() { let date = &symbol[(symbol.len() - 8)..]; assert!(date.parse::().is_ok()); assert!(symbol.contains("_USD_")); } } #[test] fn fetch_linear_future_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::LinearFuture).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { let date = &symbol[(symbol.len() - 8)..]; assert!(date.parse::().is_ok()); assert!(symbol.contains("_USDT_")); } } #[test] fn fetch_spot_markets() { let markets = fetch_markets(EXCHANGE_NAME, MarketType::Spot).unwrap(); assert!(!markets.is_empty()); let btc_usdt = markets.iter().find(|m| m.symbol == "BTC_USDT").unwrap().clone(); assert_eq!(btc_usdt.market_type, MarketType::Spot); assert!(btc_usdt.contract_value.is_none()); assert_eq!(btc_usdt.precision.tick_size, 0.1); assert_eq!(btc_usdt.precision.lot_size, 0.0001); let quantity_limit = btc_usdt.quantity_limit.unwrap(); assert_eq!(quantity_limit.min.unwrap(), 0.0001); assert!(quantity_limit.max.is_none()); } #[test] fn fetch_inverse_swap_markets() { let markets = fetch_markets(EXCHANGE_NAME, MarketType::InverseSwap).unwrap(); assert!(!markets.is_empty()); let btc_usd = markets.iter().find(|m| m.symbol == "BTC_USD").unwrap().clone(); assert_eq!(btc_usd.market_type, MarketType::InverseSwap); assert_eq!(btc_usd.contract_value, Some(1.0)); assert_eq!(btc_usd.precision.tick_size, 0.1); assert_eq!(btc_usd.precision.lot_size, 1.0); let quantity_limit = btc_usd.quantity_limit.unwrap(); assert_eq!(quantity_limit.min.unwrap(), 1.0); assert_eq!(quantity_limit.max, Some(1000000.0)); } #[test] fn fetch_linear_swap_markets() { let markets = fetch_markets(EXCHANGE_NAME, MarketType::LinearSwap).unwrap(); assert!(!markets.is_empty()); let btc_usdt = markets.iter().find(|m| m.symbol == "BTC_USDT").unwrap().clone(); assert_eq!(btc_usdt.market_type, MarketType::LinearSwap); assert_eq!(btc_usdt.contract_value, Some(0.0001)); assert_eq!(btc_usdt.precision.tick_size, 0.1); assert_eq!(btc_usdt.precision.lot_size, 0.0001); let quantity_limit = btc_usdt.quantity_limit.unwrap(); assert_eq!(quantity_limit.min.unwrap(), 1.0); assert_eq!(quantity_limit.max, Some(1000000.0)); } #[ignore = "Gate inverse future market has no trading symbols"] #[test] fn fetch_inverse_future_markets() { let markets = fetch_markets(EXCHANGE_NAME, MarketType::InverseFuture).unwrap(); assert!(!markets.is_empty()); let btc_usd = markets.iter().find(|m| m.symbol.starts_with("BTC_USD_")).unwrap().clone(); assert_eq!(btc_usd.market_type, MarketType::InverseFuture); assert_eq!(btc_usd.contract_value, Some(1.0)); assert!(btc_usd.delivery_date.is_some()); assert_eq!(btc_usd.precision.tick_size, 0.1); assert_eq!(btc_usd.precision.lot_size, 1.0); let quantity_limit = btc_usd.quantity_limit.unwrap(); assert_eq!(quantity_limit.min.unwrap(), 1.0); assert_eq!(quantity_limit.max, Some(1000000.0)); } #[test] fn fetch_linear_future_markets() { let markets = fetch_markets(EXCHANGE_NAME, MarketType::LinearFuture).unwrap(); assert!(!markets.is_empty()); let btc_usdt = markets.iter().find(|m| m.symbol.starts_with("BTC_USDT_")).unwrap().clone(); assert_eq!(btc_usdt.market_type, MarketType::LinearFuture); assert_eq!(btc_usdt.contract_value, Some(0.0001)); assert!(btc_usdt.delivery_date.is_some()); assert_eq!(btc_usdt.precision.tick_size, 0.1); assert_eq!(btc_usdt.precision.lot_size, 0.0001); let quantity_limit = btc_usdt.quantity_limit.unwrap(); assert_eq!(quantity_limit.min.unwrap(), 1.0); assert_eq!(quantity_limit.max, Some(1000000.0)); } #[test_case(MarketType::LinearFuture)] #[test_case(MarketType::LinearSwap)] // TODO: ETH_USD is actually a quanto swap contract // #[test_case(MarketType::InverseFuture)] // #[test_case(MarketType::InverseSwap)] fn test_contract_values(market_type: MarketType) { check_contract_values!(EXCHANGE_NAME, market_type); } ================================================ FILE: crypto-markets/tests/huobi.rs ================================================ use crypto_market_type::{get_market_types, MarketType}; use crypto_markets::{fetch_markets, fetch_symbols}; use crypto_pair::get_market_type; use test_case::test_case; #[macro_use] mod utils; const EXCHANGE_NAME: &str = "huobi"; #[test] fn fetch_all_symbols() { gen_all_symbols!(); } #[test] fn fetch_spot_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::Spot).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { assert_eq!(symbol.to_lowercase(), symbol.to_string()); assert_eq!(MarketType::Spot, get_market_type(symbol, EXCHANGE_NAME, None)); } } #[test] fn fetch_inverse_future_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::InverseFuture).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { assert!( symbol.ends_with("_CW") || symbol.ends_with("_NW") || symbol.ends_with("_CQ") || symbol.ends_with("_NQ") ); assert_eq!(MarketType::InverseFuture, get_market_type(symbol, EXCHANGE_NAME, None)); } } #[test] fn fetch_inverse_swap_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::InverseSwap).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { assert!(symbol.ends_with("-USD")); assert_eq!(MarketType::InverseSwap, get_market_type(symbol, EXCHANGE_NAME, None)); } } #[test] fn fetch_linear_swap_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::LinearSwap).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { assert!(symbol.ends_with("-USDT")); assert_eq!(MarketType::LinearSwap, get_market_type(symbol, EXCHANGE_NAME, None)); } } #[test] #[ignore] fn fetch_option_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::EuropeanOption).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { assert!(symbol.contains("-C-") || symbol.contains("-P-")); assert_eq!(MarketType::EuropeanOption, get_market_type(symbol, EXCHANGE_NAME, None)); } } #[test] fn fetch_spot_markets() { let markets = fetch_markets(EXCHANGE_NAME, MarketType::Spot).unwrap(); assert!(!markets.is_empty()); let btc_usdt = markets.iter().find(|m| m.symbol == "btcusdt").unwrap().clone(); assert_eq!(btc_usdt.precision.tick_size, 0.01); assert_eq!(btc_usdt.precision.lot_size, 0.000001); let quantity_limit = btc_usdt.quantity_limit.unwrap(); assert_eq!(quantity_limit.min.unwrap(), 0.0001); assert_eq!(quantity_limit.max, Some(1000.0)); } #[test] fn fetch_inverse_future_markets() { let markets = fetch_markets(EXCHANGE_NAME, MarketType::InverseFuture).unwrap(); assert!(!markets.is_empty()); let btc_usd = markets.iter().find(|m| m.symbol == "BTC_CW").unwrap().clone(); assert_eq!(btc_usd.precision.tick_size, 0.01); assert_eq!(btc_usd.precision.lot_size, 1.0); assert!(btc_usd.quantity_limit.is_none()); } #[test] fn fetch_inverse_swap_markets() { let markets = fetch_markets(EXCHANGE_NAME, MarketType::InverseSwap).unwrap(); assert!(!markets.is_empty()); let btc_usd = markets.iter().find(|m| m.symbol == "BTC-USD").unwrap().clone(); assert_eq!(btc_usd.precision.tick_size, 0.1); assert_eq!(btc_usd.precision.lot_size, 1.0); assert!(btc_usd.quantity_limit.is_none()); } #[test] fn fetch_linear_swap_markets() { let markets = fetch_markets(EXCHANGE_NAME, MarketType::LinearSwap).unwrap(); assert!(!markets.is_empty()); let btc_usdt = markets.iter().find(|m| m.symbol == "BTC-USDT").unwrap().clone(); assert_eq!(btc_usdt.precision.tick_size, 0.1); assert_eq!(btc_usdt.precision.lot_size, 1.0); assert!(btc_usdt.quantity_limit.is_none()); } #[test_case(MarketType::InverseFuture)] #[test_case(MarketType::InverseSwap)] #[test_case(MarketType::LinearSwap)] fn test_contract_values(market_type: MarketType) { check_contract_values!(EXCHANGE_NAME, market_type); } ================================================ FILE: crypto-markets/tests/kraken.rs ================================================ use crypto_market_type::{get_market_types, MarketType}; use crypto_markets::{fetch_markets, fetch_symbols}; use crypto_pair::get_market_type; #[macro_use] mod utils; const EXCHANGE_NAME: &str = "kraken"; #[test] fn fetch_all_symbols() { gen_all_symbols!(); } #[test] fn fetch_spot_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::Spot).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { assert!(symbol.contains('/')); assert_eq!(MarketType::Spot, get_market_type(symbol, EXCHANGE_NAME, None)); } } #[test] fn fetch_inverse_future_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::InverseFuture).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { assert!(symbol.starts_with("FI_")); let date = &symbol[(symbol.len() - 6)..]; assert!(date.parse::().is_ok()); assert_eq!(MarketType::InverseFuture, get_market_type(symbol, EXCHANGE_NAME, None)); } } #[test] fn fetch_inverse_swap_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::InverseSwap).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { assert!(symbol.starts_with("PI_")); assert!(symbol.ends_with("USD")); assert_eq!(MarketType::InverseSwap, get_market_type(symbol, EXCHANGE_NAME, None)); } } #[test] fn fetch_spot_markets() { let markets = fetch_markets(EXCHANGE_NAME, MarketType::Spot).unwrap(); assert!(!markets.is_empty()); let btcusd = markets.iter().find(|m| m.symbol == "XBT/USD").unwrap().clone(); assert_eq!(btcusd.precision.tick_size, 0.1); assert_eq!(btcusd.precision.lot_size, 0.00000001); let quantity_limit = btcusd.quantity_limit.unwrap(); assert_eq!(quantity_limit.min.unwrap(), 0.0001); assert_eq!(quantity_limit.max, None); } #[test] fn fetch_inverse_future_markets() { let markets = fetch_markets(EXCHANGE_NAME, MarketType::InverseFuture).unwrap(); assert!(!markets.is_empty()); let btcusd = markets.iter().find(|m| m.symbol.starts_with("fi_xbtusd_")).unwrap().clone(); assert_eq!(btcusd.precision.tick_size, 0.5); assert_eq!(btcusd.precision.lot_size, 1.0); let quantity_limit = btcusd.quantity_limit.unwrap(); assert_eq!(quantity_limit.min.unwrap(), 1.0); assert_eq!(quantity_limit.max, None); } #[test] fn fetch_inverse_swap_markets() { let markets = fetch_markets(EXCHANGE_NAME, MarketType::InverseSwap).unwrap(); assert!(!markets.is_empty()); let btcusd = markets.iter().find(|m| m.symbol == "pi_xbtusd").unwrap().clone(); assert_eq!(btcusd.precision.tick_size, 0.5); assert_eq!(btcusd.precision.lot_size, 1.0); let quantity_limit = btcusd.quantity_limit.unwrap(); assert_eq!(quantity_limit.min.unwrap(), 1.0); assert_eq!(quantity_limit.max, None); } ================================================ FILE: crypto-markets/tests/kucoin.rs ================================================ use crypto_market_type::{get_market_types, MarketType}; use crypto_markets::{fetch_markets, fetch_symbols}; use crypto_pair::get_market_type; use test_case::test_case; #[macro_use] mod utils; const EXCHANGE_NAME: &str = "kucoin"; #[test] fn fetch_all_symbols() { gen_all_symbols!(); } #[test] fn fetch_spot_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::Spot).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { assert!(symbol.contains('-')); assert_eq!(symbol.to_string(), symbol.to_uppercase()); assert_eq!(MarketType::Spot, get_market_type(symbol, EXCHANGE_NAME, None)); } } #[test] fn fetch_inverse_swap_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::InverseSwap).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { assert!(symbol.ends_with("USDM")); assert_eq!(MarketType::InverseSwap, get_market_type(symbol, EXCHANGE_NAME, None)); } } #[test] fn fetch_linear_swap_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::LinearSwap).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { assert!(symbol.ends_with("USDTM") || symbol.ends_with("USDCM")); assert_eq!(MarketType::LinearSwap, get_market_type(symbol, EXCHANGE_NAME, None)); } } #[test] fn fetch_inverse_future_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::InverseFuture).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { let date = &symbol[(symbol.len() - 2)..]; assert!(date.parse::().is_ok()); } } #[test] fn fetch_spot_markets() { let markets = fetch_markets(EXCHANGE_NAME, MarketType::Spot).unwrap(); assert!(!markets.is_empty()); let btc_usdt = markets.iter().find(|m| m.symbol == "BTC-USDT").unwrap().clone(); assert_eq!(btc_usdt.market_type, MarketType::Spot); assert!(btc_usdt.contract_value.is_none()); assert_eq!(btc_usdt.precision.tick_size, 0.1); assert_eq!(btc_usdt.precision.lot_size, 0.00000001); let quantity_limit = btc_usdt.quantity_limit.unwrap(); assert_eq!(quantity_limit.min.unwrap(), 0.00001); assert_eq!(quantity_limit.max, Some(10000000000.0)); } #[test] fn fetch_inverse_future_markets() { let markets = fetch_markets(EXCHANGE_NAME, MarketType::InverseFuture).unwrap(); assert!(!markets.is_empty()); let btcusd = markets.iter().find(|m| m.base == "BTC" && m.quote == "USD").unwrap().clone(); assert_eq!(btcusd.market_type, MarketType::InverseFuture); assert_eq!(btcusd.contract_value, Some(1.0)); assert_eq!(btcusd.precision.tick_size, 1.0); assert_eq!(btcusd.precision.lot_size, 1.0); assert!(btcusd.quantity_limit.is_none()); } #[test] fn fetch_inverse_swap_markets() { let markets = fetch_markets(EXCHANGE_NAME, MarketType::InverseSwap).unwrap(); assert!(!markets.is_empty()); let btcusd_perp = markets.iter().find(|m| m.symbol == "XBTUSDM").unwrap().clone(); assert_eq!(btcusd_perp.market_type, MarketType::InverseSwap); assert_eq!(btcusd_perp.contract_value, Some(1.0)); assert_eq!(btcusd_perp.precision.tick_size, 1.0); assert_eq!(btcusd_perp.precision.lot_size, 1.0); assert!(btcusd_perp.quantity_limit.is_none()); } #[test] fn fetch_linear_swap_markets() { let markets = fetch_markets(EXCHANGE_NAME, MarketType::LinearSwap).unwrap(); assert!(!markets.is_empty()); let btcusdt = markets.iter().find(|m| m.symbol == "XBTUSDTM").unwrap().clone(); assert_eq!(btcusdt.market_type, MarketType::LinearSwap); assert_eq!(btcusdt.contract_value, Some(0.001)); assert_eq!(btcusdt.precision.tick_size, 1.0); assert_eq!(btcusdt.precision.lot_size, 1.0); assert!(btcusdt.quantity_limit.is_none()); } #[test_case(MarketType::InverseFuture)] #[test_case(MarketType::InverseSwap)] #[test_case(MarketType::LinearSwap)] fn test_contract_values(market_type: MarketType) { check_contract_values!(EXCHANGE_NAME, market_type); } ================================================ FILE: crypto-markets/tests/mexc.rs ================================================ use crypto_market_type::MarketType; use crypto_markets::{fetch_markets, fetch_symbols}; use crypto_pair::get_market_type; use test_case::test_case; #[macro_use] mod utils; const EXCHANGE_NAME: &str = "mexc"; #[test] fn fetch_spot_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::Spot).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { assert!(symbol.contains('_')); assert_eq!(symbol.to_uppercase(), symbol.to_string()); assert_eq!(MarketType::Spot, get_market_type(symbol, EXCHANGE_NAME, Some(true))); } } #[test] fn fetch_linear_swap_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::LinearSwap).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { assert!(symbol.ends_with("_USDT")); assert_eq!(MarketType::LinearSwap, get_market_type(symbol, EXCHANGE_NAME, None)); } } #[test] fn fetch_inverse_swap_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::InverseSwap).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { assert!(symbol.ends_with("_USD")); assert_eq!(MarketType::InverseSwap, get_market_type(symbol, EXCHANGE_NAME, None)); } } #[test] fn fetch_spot_markets() { let markets = fetch_markets(EXCHANGE_NAME, MarketType::Spot).unwrap(); assert!(!markets.is_empty()); let btc_usdt = markets.iter().find(|m| m.symbol == "BTC_USDT").unwrap().clone(); assert_eq!(btc_usdt.market_type, MarketType::Spot); assert!(btc_usdt.contract_value.is_none()); assert_eq!(btc_usdt.precision.tick_size, 0.01); assert_eq!(btc_usdt.precision.lot_size, 0.000001); let quantity_limit = btc_usdt.quantity_limit.unwrap(); assert_eq!(quantity_limit.min.unwrap(), 5.0); assert_eq!(quantity_limit.max, Some(5000000.0)); } #[test] fn fetch_inverse_swap_markets() { let markets = fetch_markets(EXCHANGE_NAME, MarketType::InverseSwap).unwrap(); assert!(!markets.is_empty()); let btcusd_perp = markets.iter().find(|m| m.symbol == "BTC_USD").unwrap().clone(); assert_eq!(btcusd_perp.market_type, MarketType::InverseSwap); assert_eq!(btcusd_perp.contract_value, Some(100.0)); assert_eq!(btcusd_perp.precision.tick_size, 0.1); assert_eq!(btcusd_perp.precision.lot_size, 1.0); let quantity_limit = btcusd_perp.quantity_limit.unwrap(); assert_eq!(quantity_limit.min.unwrap(), 1.0); assert_eq!(quantity_limit.max, Some(30000.0)); } #[test] fn fetch_linear_swap_markets() { let markets = fetch_markets(EXCHANGE_NAME, MarketType::LinearSwap).unwrap(); assert!(!markets.is_empty()); let btcusdt = markets.iter().find(|m| m.symbol == "BTC_USDT").unwrap().clone(); assert_eq!(btcusdt.market_type, MarketType::LinearSwap); assert_eq!(btcusdt.contract_value, Some(0.0001)); assert_eq!(btcusdt.precision.tick_size, 0.1); assert_eq!(btcusdt.precision.lot_size, 1.0); let quantity_limit = btcusdt.quantity_limit.unwrap(); assert_eq!(quantity_limit.min.unwrap(), 1.0); assert_eq!(quantity_limit.max, Some(2625000.0)); } #[test_case(MarketType::InverseSwap)] #[test_case(MarketType::LinearSwap)] fn test_contract_values(market_type: MarketType) { check_contract_values!(EXCHANGE_NAME, market_type); } ================================================ FILE: crypto-markets/tests/okx.rs ================================================ use crypto_market_type::{get_market_types, MarketType}; use crypto_markets::{fetch_markets, fetch_symbols}; use crypto_pair::get_market_type; use test_case::test_case; #[macro_use] mod utils; const EXCHANGE_NAME: &str = "okx"; #[test] fn fetch_all_symbols() { gen_all_symbols!(); } #[test] fn fetch_spot_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::Spot).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { assert!(symbol.contains('-')); assert_eq!(MarketType::Spot, get_market_type(symbol, EXCHANGE_NAME, None)); } } #[test] fn fetch_inverse_future_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::InverseFuture).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { let date = &symbol[(symbol.len() - 6)..]; assert!(date.parse::().is_ok()); assert!(symbol.contains("-USD-")); assert_eq!(MarketType::InverseFuture, get_market_type(symbol, EXCHANGE_NAME, None)); } } #[test] fn fetch_linear_future_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::LinearFuture).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { let date = &symbol[(symbol.len() - 6)..]; assert!(date.parse::().is_ok()); assert!(symbol.contains("-USDT-") || symbol.contains("-USDC-")); assert_eq!(MarketType::LinearFuture, get_market_type(symbol, EXCHANGE_NAME, None)); } } #[test] fn fetch_inverse_swap_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::InverseSwap).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { assert!(symbol.ends_with("-USD-SWAP")); assert_eq!(MarketType::InverseSwap, get_market_type(symbol, EXCHANGE_NAME, None)); } } #[test] fn fetch_linear_swap_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::LinearSwap).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { assert!(symbol.ends_with("-USDT-SWAP") || symbol.ends_with("-USDC-SWAP")); assert_eq!(MarketType::LinearSwap, get_market_type(symbol, EXCHANGE_NAME, None)); } } #[test] fn fetch_option_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::EuropeanOption).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { assert!(symbol.ends_with("-C") || symbol.ends_with("-P")); assert_eq!(MarketType::EuropeanOption, get_market_type(symbol, EXCHANGE_NAME, None)); } } #[test] fn fetch_spot_markets() { let markets = fetch_markets(EXCHANGE_NAME, MarketType::Spot).unwrap(); assert!(!markets.is_empty()); let btc_usdt = markets.iter().find(|m| m.symbol == "BTC-USDT").unwrap().clone(); assert_eq!(btc_usdt.precision.tick_size, 0.1); assert_eq!(btc_usdt.precision.lot_size, 0.00000001); let quantity_limit = btc_usdt.quantity_limit.unwrap(); assert_eq!(quantity_limit.min.unwrap(), 0.00001); assert_eq!(quantity_limit.max, None); } #[test] fn fetch_inverse_future_markets() { let markets = fetch_markets(EXCHANGE_NAME, MarketType::InverseFuture).unwrap(); assert!(!markets.is_empty()); let btc_usd = markets.iter().find(|m| m.symbol.starts_with("BTC-USD-")).unwrap().clone(); assert_eq!(btc_usd.precision.tick_size, 0.1); assert_eq!(btc_usd.precision.lot_size, 1.0); let quantity_limit = btc_usd.quantity_limit.unwrap(); assert_eq!(quantity_limit.min.unwrap(), 1.0); assert_eq!(quantity_limit.max, None); } #[test] fn fetch_linear_future_markets() { let markets = fetch_markets(EXCHANGE_NAME, MarketType::LinearFuture).unwrap(); assert!(!markets.is_empty()); let btc_usdt = markets.iter().find(|m| m.symbol.starts_with("BTC-USDT-")).unwrap().clone(); assert_eq!(btc_usdt.precision.tick_size, 0.1); assert_eq!(btc_usdt.precision.lot_size, 1.0); let quantity_limit = btc_usdt.quantity_limit.unwrap(); assert_eq!(quantity_limit.min.unwrap(), 1.0); assert_eq!(quantity_limit.max, None); } #[test] fn fetch_inverse_swap_markets() { let markets = fetch_markets(EXCHANGE_NAME, MarketType::InverseSwap).unwrap(); assert!(!markets.is_empty()); let btc_usd = markets.iter().find(|m| m.symbol == "BTC-USD-SWAP").unwrap().clone(); assert_eq!(btc_usd.precision.tick_size, 0.1); assert_eq!(btc_usd.precision.lot_size, 1.0); let quantity_limit = btc_usd.quantity_limit.unwrap(); assert_eq!(quantity_limit.min.unwrap(), 1.0); assert_eq!(quantity_limit.max, None); } #[test] fn fetch_linear_swap_markets() { let markets = fetch_markets(EXCHANGE_NAME, MarketType::LinearSwap).unwrap(); assert!(!markets.is_empty()); let btc_usdt = markets.iter().find(|m| m.symbol == "BTC-USDT-SWAP").unwrap().clone(); assert_eq!(btc_usdt.precision.tick_size, 0.1); assert_eq!(btc_usdt.precision.lot_size, 1.0); let quantity_limit = btc_usdt.quantity_limit.unwrap(); assert_eq!(quantity_limit.min.unwrap(), 1.0); assert_eq!(quantity_limit.max, None); } #[test] fn fetch_option_markets() { let markets = fetch_markets(EXCHANGE_NAME, MarketType::EuropeanOption).unwrap(); assert!(!markets.is_empty()); let btc_usd = markets.iter().find(|m| m.symbol.starts_with("BTC-USD-")).unwrap().clone(); assert_eq!(btc_usd.precision.tick_size, 0.0005); assert_eq!(btc_usd.precision.lot_size, 1.0); let quantity_limit = btc_usd.quantity_limit.unwrap(); assert_eq!(quantity_limit.min.unwrap(), 1.0); assert_eq!(quantity_limit.max, None); } // #[test_case(MarketType::InverseFuture)] // #[test_case(MarketType::LinearFuture)] // #[test_case(MarketType::InverseSwap)] // #[test_case(MarketType::LinearSwap)] #[test_case(MarketType::EuropeanOption)] fn test_contract_values(market_type: MarketType) { check_contract_values!(EXCHANGE_NAME, market_type); } ================================================ FILE: crypto-markets/tests/utils/mod.rs ================================================ #[allow(unused_macros)] macro_rules! gen_all_symbols { () => { let market_types = get_market_types(EXCHANGE_NAME); assert!(!market_types.is_empty()); for market_type in market_types.into_iter().filter(|m| m != &MarketType::Unknown) { let symbols = fetch_symbols(EXCHANGE_NAME, market_type).unwrap(); if EXCHANGE_NAME != "gate" && market_type != MarketType::InverseFuture { assert!(!symbols.is_empty()); } } }; } #[allow(unused_macros)] macro_rules! check_contract_values { ($exchange:expr, $market_type:expr) => {{ let markets = fetch_markets($exchange, $market_type).unwrap(); for market in markets.into_iter().filter(|m| { m.market_type == MarketType::InverseSwap || m.market_type == MarketType::LinearSwap || m.market_type == MarketType::InverseFuture || m.market_type == MarketType::LinearFuture }) { let contract_value = crypto_contract_value::get_contract_value( &market.exchange, market.market_type, format!("{}/{}", market.base, market.quote).as_str(), ); assert_eq!(market.contract_value, contract_value); if market.base != crypto_pair::normalize_currency(market.base_id.as_str(), $exchange) { println!("{}", serde_json::to_string(&market).unwrap()); } assert_eq!( market.base, crypto_pair::normalize_currency(market.base_id.as_str(), $exchange) ); assert_eq!( market.quote, crypto_pair::normalize_currency(market.quote_id.as_str(), $exchange) ); assert_eq!( market.settle.unwrap(), crypto_pair::normalize_currency(market.settle_id.unwrap().as_str(), $exchange) ); // assert!(market.margin); if market.market_type == MarketType::InverseFuture || market.market_type == MarketType::LinearFuture || market.market_type == MarketType::QuantoFuture || market.market_type == MarketType::EuropeanOption { assert!(market.delivery_date.is_some()); } else { assert!(market.delivery_date.is_none()); } } }}; } ================================================ FILE: crypto-markets/tests/zb.rs ================================================ use crypto_market_type::{get_market_types, MarketType}; use crypto_markets::{fetch_markets, fetch_symbols}; use crypto_pair::get_market_type; use test_case::test_case; #[macro_use] mod utils; const EXCHANGE_NAME: &str = "zb"; #[test] fn fetch_all_symbols() { gen_all_symbols!(); } #[test] fn fetch_spot_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::Spot).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { assert!(symbol.contains('_')); assert!( symbol.ends_with("_usdt") || symbol.ends_with("_usdc") || symbol.ends_with("_qc") || symbol.ends_with("_btc") ); assert_eq!(symbol.to_string(), symbol.to_lowercase()); assert_eq!(MarketType::Spot, get_market_type(symbol, EXCHANGE_NAME, None)); } } #[test] fn fetch_linear_swap_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::LinearSwap).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { assert!(symbol.ends_with("_USDT")); // assert!(symbol.ends_with("_USDT") || symbol.ends_with("_QC")); assert_eq!(MarketType::LinearSwap, get_market_type(symbol, EXCHANGE_NAME, None)); } } #[test] fn fetch_spot_markets() { let markets = fetch_markets(EXCHANGE_NAME, MarketType::Spot).unwrap(); assert!(!markets.is_empty()); let btc_usdt = markets.iter().find(|m| m.symbol == "btc_usdt").unwrap().clone(); assert_eq!(btc_usdt.market_type, MarketType::Spot); assert!(btc_usdt.contract_value.is_none()); assert_eq!(btc_usdt.precision.tick_size, 0.01); assert_eq!(btc_usdt.precision.lot_size, 0.0001); let quantity_limit = btc_usdt.quantity_limit.unwrap(); assert_eq!(quantity_limit.min.unwrap(), 0.0001); assert!(quantity_limit.max.is_none()); } #[test] fn fetch_linear_swap_markets() { let markets = fetch_markets(EXCHANGE_NAME, MarketType::LinearSwap).unwrap(); assert!(!markets.is_empty()); let btc_usdt = markets.iter().find(|m| m.symbol == "BTC_USDT").unwrap().clone(); assert_eq!(btc_usdt.market_type, MarketType::LinearSwap); assert_eq!(btc_usdt.contract_value, Some(1.0)); assert_eq!(btc_usdt.precision.tick_size, 0.01); assert_eq!(btc_usdt.precision.lot_size, 0.001); let quantity_limit = btc_usdt.quantity_limit.unwrap(); assert_eq!(quantity_limit.min.unwrap(), 0.001); assert_eq!(quantity_limit.max, Some(1000.0)); } #[test_case(MarketType::LinearSwap)] fn test_contract_values(market_type: MarketType) { check_contract_values!(EXCHANGE_NAME, market_type); } ================================================ FILE: crypto-markets/tests/zbg.rs ================================================ use crypto_market_type::{get_market_types, MarketType}; use crypto_markets::{fetch_markets, fetch_symbols}; use crypto_pair::get_market_type; use test_case::test_case; #[macro_use] mod utils; const EXCHANGE_NAME: &str = "zbg"; #[test] fn fetch_all_symbols() { gen_all_symbols!(); } #[test] fn fetch_spot_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::Spot).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { assert!(symbol.contains('_')); assert_eq!(symbol.to_string(), symbol.to_lowercase()); assert_eq!(MarketType::Spot, get_market_type(symbol, EXCHANGE_NAME, None)); } } #[test] fn fetch_inverse_swap_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::InverseSwap).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { assert!(symbol.ends_with("_USD-R")); assert_eq!(MarketType::InverseSwap, get_market_type(symbol, EXCHANGE_NAME, None)); } } #[test] fn fetch_linear_swap_symbols() { let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::LinearSwap).unwrap(); assert!(!symbols.is_empty()); for symbol in symbols.iter() { assert!(symbol.ends_with("_USDT") || symbol.ends_with("_ZUSD")); assert_eq!(MarketType::LinearSwap, get_market_type(symbol, EXCHANGE_NAME, None)); } } #[test] fn fetch_spot_markets() { let markets = fetch_markets(EXCHANGE_NAME, MarketType::Spot).unwrap(); assert!(!markets.is_empty()); let btc_usdt = markets.iter().find(|m| m.symbol == "btc_usdt").unwrap().clone(); assert_eq!(btc_usdt.market_type, MarketType::Spot); assert!(btc_usdt.contract_value.is_none()); assert_eq!(btc_usdt.precision.tick_size, 0.1); assert_eq!(btc_usdt.precision.lot_size, 0.000001); let quantity_limit = btc_usdt.quantity_limit.unwrap(); assert_eq!(quantity_limit.min.unwrap(), 0.000001); assert!(quantity_limit.max.is_none()); } #[test] fn fetch_inverse_swap_markets() { let markets = fetch_markets(EXCHANGE_NAME, MarketType::InverseSwap).unwrap(); assert!(!markets.is_empty()); let btcusd_perp = markets.iter().find(|m| m.symbol == "BTC_USD-R").unwrap().clone(); assert_eq!(btcusd_perp.market_type, MarketType::InverseSwap); assert_eq!(btcusd_perp.contract_value, Some(1.0)); assert_eq!(btcusd_perp.precision.tick_size, 0.5); assert_eq!(btcusd_perp.precision.lot_size, 1.0); assert!(btcusd_perp.quantity_limit.is_none()); } #[test] fn fetch_linear_swap_markets() { let markets = fetch_markets(EXCHANGE_NAME, MarketType::LinearSwap).unwrap(); assert!(!markets.is_empty()); let btcusdt = markets.iter().find(|m| m.symbol == "BTC_USDT").unwrap().clone(); assert_eq!(btcusdt.market_type, MarketType::LinearSwap); assert_eq!(btcusdt.contract_value, Some(0.01)); assert_eq!(btcusdt.precision.tick_size, 0.5); assert_eq!(btcusdt.precision.lot_size, 1.0); assert!(btcusdt.quantity_limit.is_none()); } #[test_case(MarketType::InverseSwap)] #[test_case(MarketType::LinearSwap)] fn test_contract_values(market_type: MarketType) { check_contract_values!(EXCHANGE_NAME, market_type); } ================================================ FILE: crypto-msg-type/Cargo.toml ================================================ [package] name = "crypto-msg-type" version = "1.0.11" authors = ["soulmachine "] edition = "2021" description = "Cryptocurrenty message type" license = "Apache-2.0" repository = "https://github.com/crypto-crawler/crypto-crawler-rs/tree/main/crypto-msg-type" keywords = ["cryptocurrency", "blockchain", "trading"] [dependencies] serde = { version = "1", features = ["derive"] } serde_json = "1" strum = "0.24" strum_macros = "0.24" ================================================ FILE: crypto-msg-type/include/crypto_msg_type.h ================================================ /* Licensed under Apache-2.0 */ #ifndef CRYPTO_MSG_TYPE_H_ #define CRYPTO_MSG_TYPE_H_ /** * Crypto message types. * * L2Snapshot and L2TopK are very similar, the former is from RESTful API, * the latter is from websocket. */ typedef enum { /** * All other messages */ Other, /** * tick-by-tick trade messages */ Trade, /** * Incremental level2 orderbook updates */ L2Event, /** * Level2 snapshot from RESTful API */ L2Snapshot, /** * Level2 top K snapshots from websocket */ L2TopK, /** * Incremental level3 orderbook updates */ L3Event, /** * Level3 snapshot from RESTful API */ L3Snapshot, /** * Best bid and ask */ BBO, /** * 24hr rolling window ticker */ Ticker, /** * OHLCV candlestick */ Candlestick, /** * Funding rate */ FundingRate, /** * Open interest */ OpenInterest, /** * Long/short ratio */ LongShortRatio, /** * Taker buy/sell volume */ TakerVolume, } MessageType; #endif /* CRYPTO_MSG_TYPE_H_ */ ================================================ FILE: crypto-msg-type/src/exchanges/binance.rs ================================================ use std::collections::HashMap; use crate::MessageType; fn msg_type_to_channel(msg_type: MessageType) -> &'static str { match msg_type { MessageType::Trade => "aggTrade", MessageType::L2Event => "depth@100ms", MessageType::L2TopK => "depth5", MessageType::BBO => "bookTicker", MessageType::Ticker => "ticker", MessageType::Candlestick => "kline", _ => panic!("Unknown message type {msg_type}"), } } fn channel_symbol_to_topic( channel: &str, symbol: &str, configs: Option<&HashMap>, ) -> String { if channel == "kline" { format!("{}@kline_{}", symbol.to_lowercase(), configs.unwrap().get("interval").unwrap()) } else { format!("{}@{}", symbol.to_lowercase(), channel) } } fn topics_to_command(topics: &[String], subscribe: bool) -> String { // spot requires `id`, otherwise it returns the error: // {"error":{"code":2,"msg":"Invalid request: request ID must be an unsigned // integer"}} format!( r#"{{"id":9527, "method":"{}","params":{}}}"#, if subscribe { "SUBSCRIBE" } else { "UNSUBSCRIBE" }, serde_json::to_string(topics).unwrap() ) } pub(crate) fn get_ws_commands( msg_types: &[MessageType], symbols: &[String], subscribe: bool, configs: Option<&HashMap>, ) -> Vec { let topics = msg_types .iter() .map(|msg_type| msg_type_to_channel(*msg_type)) .flat_map(|channel| { symbols.iter().map(|symbol| channel_symbol_to_topic(channel, symbol, configs)) }) .collect::>(); vec![topics_to_command(&topics, subscribe)] } #[cfg(test)] mod tests { use super::*; #[test] fn single_msg_type_multiple_symbols() { let commands = get_ws_commands( &[MessageType::Trade], &["BTCUSDT".to_string(), "ETHUSDT".to_string()], true, None, ); assert_eq!(commands.len(), 1); assert_eq!( r#"{"id":9527, "method":"SUBSCRIBE","params":["btcusdt@aggTrade","ethusdt@aggTrade"]}"#, commands[0] ); } #[test] fn multiple_msg_types_single_symbol() { let commands = get_ws_commands( &[MessageType::Trade, MessageType::L2Event], &["BTCUSDT".to_string()], true, None, ); assert_eq!(commands.len(), 1); assert_eq!( r#"{"id":9527, "method":"SUBSCRIBE","params":["btcusdt@aggTrade","btcusdt@depth@100ms"]}"#, commands[0] ); } #[test] fn candlestick() { let mut configs = HashMap::new(); configs.insert("interval".to_string(), "1m".to_string()); let commands = get_ws_commands( &[MessageType::Candlestick], &["BTCUSDT".to_string(), "ETHUSDT".to_string()], true, Some(&configs), ); assert_eq!(commands.len(), 1); assert_eq!( r#"{"id":9527, "method":"SUBSCRIBE","params":["btcusdt@kline_1m","ethusdt@kline_1m"]}"#, commands[0] ); } } ================================================ FILE: crypto-msg-type/src/exchanges/bitfinex.rs ================================================ use std::collections::HashMap; use crate::MessageType; fn msg_type_symbol_to_command( msg_type: MessageType, symbol: &str, subscribe: bool, configs: Option<&HashMap>, ) -> String { let sub_or_unsub = if subscribe { "subscribe" } else { "unsubscribe" }; match msg_type { MessageType::Trade | MessageType::Ticker => { let channel = if msg_type == MessageType::Trade { "trades" } else { "ticker" }; format!(r#"{{"event":"{sub_or_unsub}", "channel":"{channel}", "symbol":"{symbol}"}}"#) } MessageType::L2Event => format!( r#"{{"event":"{sub_or_unsub}", "channel":"book", "symbol":"{symbol}", "prec":"P0", "frec":"F0", "len":25}}"# ), MessageType::L3Event | MessageType::BBO => format!( r#"{{"event":"{}", "channel":"book", "symbol": "{}", "prec":"R0", "len": {}}}"#, sub_or_unsub, symbol, if msg_type == MessageType::L3Event { 25 } else { 1 } ), MessageType::Candlestick => format!( r#"{{"event":"{}", "channel":"candles", "key":"trade:{}:{}"}}"#, sub_or_unsub, configs.unwrap().get("interval").unwrap(), symbol ), _ => panic!("Unknown message type {msg_type}"), } } pub(crate) fn get_ws_commands( msg_types: &[MessageType], symbols: &[String], subscribe: bool, configs: Option<&HashMap>, ) -> Vec { msg_types .iter() .flat_map(|msg_type| { symbols .iter() .map(|symbol| msg_type_symbol_to_command(*msg_type, symbol, subscribe, configs)) }) .collect::>() } #[cfg(test)] mod tests { use super::*; #[test] fn single_msg_type_multiple_symbols() { let commands = get_ws_commands( &[MessageType::Trade], &["tBTCUST".to_string(), "tETHUST".to_string()], true, None, ); assert_eq!(commands.len(), 2); assert_eq!(r#"{"event":"subscribe", "channel":"trades", "symbol":"tBTCUST"}"#, commands[0]); assert_eq!(r#"{"event":"subscribe", "channel":"trades", "symbol":"tETHUST"}"#, commands[1]); } #[test] fn multiple_msg_types_single_symbol() { let commands = get_ws_commands( &[MessageType::Trade, MessageType::L2Event], &["tBTCUST".to_string()], true, None, ); assert_eq!(commands.len(), 2); assert_eq!(r#"{"event":"subscribe", "channel":"trades", "symbol":"tBTCUST"}"#, commands[0]); assert_eq!( r#"{"event":"subscribe", "channel":"book", "symbol":"tBTCUST", "prec":"P0", "frec":"F0", "len":25}"#, commands[1] ); } #[test] fn candlestick() { let mut configs = HashMap::new(); configs.insert("interval".to_string(), "1m".to_string()); let commands = get_ws_commands( &[MessageType::Candlestick], &["tBTCUST".to_string(), "tETHUST".to_string()], true, Some(&configs), ); assert_eq!(commands.len(), 2); assert_eq!( r#"{"event":"subscribe", "channel":"candles", "key":"trade:1m:tBTCUST"}"#, commands[0] ); assert_eq!( r#"{"event":"subscribe", "channel":"candles", "key":"trade:1m:tETHUST"}"#, commands[1] ); } } ================================================ FILE: crypto-msg-type/src/exchanges/bitmex.rs ================================================ use std::collections::HashMap; use crate::MessageType; fn msg_type_to_channel(msg_type: MessageType) -> &'static str { match msg_type { MessageType::Trade => "trade", MessageType::L2Event => "orderBookL2_25", MessageType::L2TopK => "orderBook10", MessageType::BBO => "quote", MessageType::Candlestick => "tradeBin", _ => panic!("Unknown message type {msg_type}"), } } fn channel_symbol_to_topic( channel: &str, symbol: &str, configs: Option<&HashMap>, ) -> String { if channel == "tradeBin" { format!("tradeBin{}:{}", configs.unwrap().get("interval").unwrap(), symbol) } else { format!("{channel}:{symbol}") } } fn topics_to_command(topics: &[String], subscribe: bool) -> String { format!( r#"{{"op":"{}", "args":{}}}"#, if subscribe { "subscribe" } else { "unsubscribe" }, serde_json::to_string(&topics).unwrap() ) } pub(crate) fn get_ws_commands( msg_types: &[MessageType], symbols: &[String], subscribe: bool, configs: Option<&HashMap>, ) -> Vec { let topics = msg_types .iter() .map(|msg_type| msg_type_to_channel(*msg_type)) .flat_map(|channel| { symbols.iter().map(|symbol| channel_symbol_to_topic(channel, symbol, configs)) }) .collect::>(); vec![topics_to_command(&topics, subscribe)] } #[cfg(test)] mod tests { use super::*; #[test] fn single_msg_type_multiple_symbols() { let commands = get_ws_commands( &[MessageType::Trade], &["XBTUSD".to_string(), "ETHUSD".to_string()], true, None, ); assert_eq!(commands.len(), 1); assert_eq!(r#"{"op":"subscribe", "args":["trade:XBTUSD","trade:ETHUSD"]}"#, commands[0]); } #[test] fn multiple_msg_types_single_symbol() { let commands = get_ws_commands( &[MessageType::Trade, MessageType::L2Event], &["XBTUSD".to_string()], true, None, ); assert_eq!(commands.len(), 1); assert_eq!( r#"{"op":"subscribe", "args":["trade:XBTUSD","orderBookL2_25:XBTUSD"]}"#, commands[0] ); } #[test] fn candlestick() { let mut configs = HashMap::new(); configs.insert("interval".to_string(), "1m".to_string()); let commands = get_ws_commands( &[MessageType::Candlestick], &["XBTUSD".to_string(), "ETHUSD".to_string()], true, Some(&configs), ); assert_eq!(commands.len(), 1); assert_eq!( r#"{"op":"subscribe", "args":["tradeBin1m:XBTUSD","tradeBin1m:ETHUSD"]}"#, commands[0] ); } } ================================================ FILE: crypto-msg-type/src/exchanges/bybit.rs ================================================ use std::collections::HashMap; use crate::MessageType; fn msg_type_to_channel(msg_type: MessageType) -> &'static str { match msg_type { MessageType::Trade => "trade", MessageType::L2Event => "orderBookL2_25", MessageType::Ticker => "instrument_info.100ms", MessageType::Candlestick => "klineV2", _ => panic!("Unknown message type {msg_type}"), } } fn channel_symbol_to_topic( channel: &str, symbol: &str, configs: Option<&HashMap>, ) -> String { if channel == "klineV2" { let interval_str = configs.unwrap().get("interval").unwrap(); if symbol.ends_with("USDT") { format!("candle.{interval_str}.{symbol}") } else { format!("klineV2.{interval_str}.{symbol}") } } else { format!("{channel}.{symbol}") } } fn topics_to_command(topics: &[String], subscribe: bool) -> String { format!( r#"{{"op":"{}", "args":{}}}"#, if subscribe { "subscribe" } else { "unsubscribe" }, serde_json::to_string(&topics).unwrap() ) } pub(crate) fn get_ws_commands( msg_types: &[MessageType], symbols: &[String], subscribe: bool, configs: Option<&HashMap>, ) -> Vec { let topics = msg_types .iter() .map(|msg_type| msg_type_to_channel(*msg_type)) .flat_map(|channel| { symbols.iter().map(|symbol| channel_symbol_to_topic(channel, symbol, configs)) }) .collect::>(); vec![topics_to_command(&topics, subscribe)] } #[cfg(test)] mod tests { use super::*; #[test] fn single_msg_type_multiple_symbols() { let commands = get_ws_commands( &[MessageType::Trade], &["BTCUSD".to_string(), "ETHUSD".to_string()], true, None, ); assert_eq!(commands.len(), 1); assert_eq!(r#"{"op":"subscribe", "args":["trade.BTCUSD","trade.ETHUSD"]}"#, commands[0]); } #[test] fn multiple_msg_types_single_symbol() { let commands = get_ws_commands( &[MessageType::Trade, MessageType::L2Event], &["BTCUSD".to_string()], true, None, ); assert_eq!(commands.len(), 1); assert_eq!( r#"{"op":"subscribe", "args":["trade.BTCUSD","orderBookL2_25.BTCUSD"]}"#, commands[0] ); } #[test] fn candlestick() { let mut configs = HashMap::new(); configs.insert("interval".to_string(), "1".to_string()); let commands = get_ws_commands( &[MessageType::Candlestick], &["BTCUSD".to_string(), "ETHUSD".to_string()], true, Some(&configs), ); assert_eq!(commands.len(), 1); assert_eq!( r#"{"op":"subscribe", "args":["klineV2.1.BTCUSD","klineV2.1.ETHUSD"]}"#, commands[0] ); } } ================================================ FILE: crypto-msg-type/src/exchanges/deribit.rs ================================================ use std::collections::HashMap; use crate::MessageType; fn msg_type_symbol_to_topic( msg_type: MessageType, symbol: &str, configs: Option<&HashMap>, ) -> String { match msg_type { MessageType::Trade => format!("trades.{symbol}.100ms"), MessageType::L2Event => format!("book.{symbol}.100ms"), MessageType::L2TopK => format!("book.{symbol}.5.10.100ms"), MessageType::BBO => format!("quote.{symbol}"), MessageType::Candlestick => { format!("chart.trades.{}.{}", symbol, configs.unwrap().get("interval").unwrap()) } MessageType::Ticker => format!("ticker.{symbol}.100ms"), _ => panic!("Unknown message type {msg_type}"), } } fn topics_to_command(topics: &[String], subscribe: bool) -> String { format!( r#"{{"method":"public/{}", "params":{{"channels":{}}}}}"#, if subscribe { "subscribe" } else { "unsubscribe" }, serde_json::to_string(topics).unwrap() ) } pub(crate) fn get_ws_commands( msg_types: &[MessageType], symbols: &[String], subscribe: bool, configs: Option<&HashMap>, ) -> Vec { let topics = msg_types .iter() .flat_map(|msg_type| { symbols.iter().map(|symbol| msg_type_symbol_to_topic(*msg_type, symbol, configs)) }) .collect::>(); vec![topics_to_command(&topics, subscribe)] } #[cfg(test)] mod tests { use super::*; #[test] fn single_msg_type_multiple_symbols() { let commands = get_ws_commands( &[MessageType::Trade], &["BTC-PERPETUAL".to_string(), "ETH-PERPETUAL".to_string()], true, None, ); assert_eq!(commands.len(), 1); assert_eq!( r#"{"method":"public/subscribe", "params":{"channels":["trades.BTC-PERPETUAL.100ms","trades.ETH-PERPETUAL.100ms"]}}"#, commands[0] ); } #[test] fn multiple_msg_types_single_symbol() { let commands = get_ws_commands( &[MessageType::Trade, MessageType::L2Event], &["BTC-PERPETUAL".to_string()], true, None, ); assert_eq!(commands.len(), 1); assert_eq!( r#"{"method":"public/subscribe", "params":{"channels":["trades.BTC-PERPETUAL.100ms","book.BTC-PERPETUAL.100ms"]}}"#, commands[0] ); } #[test] fn candlestick() { let mut configs = HashMap::new(); configs.insert("interval".to_string(), "1m".to_string()); let commands = get_ws_commands( &[MessageType::Candlestick], &["BTC-PERPETUAL".to_string(), "ETH-PERPETUAL".to_string()], true, Some(&configs), ); assert_eq!(commands.len(), 1); assert_eq!( r#"{"method":"public/subscribe", "params":{"channels":["chart.trades.BTC-PERPETUAL.1m","chart.trades.ETH-PERPETUAL.1m"]}}"#, commands[0] ); } } ================================================ FILE: crypto-msg-type/src/exchanges/ftx.rs ================================================ use std::collections::HashMap; use crate::MessageType; fn msg_type_to_channel(msg_type: MessageType) -> &'static str { match msg_type { MessageType::Trade => "trades", MessageType::L2Event => "orderbook", MessageType::BBO => "ticker", _ => panic!("Unknown message type {msg_type}"), } } fn channel_symbol_to_command(channel: &str, symbol: &str, subscribe: bool) -> String { format!( r#"{{"op":"{}","channel":"{}","market":"{}"}}"#, if subscribe { "subscribe" } else { "unsubscribe" }, channel, symbol, ) } pub(crate) fn get_ws_commands( msg_types: &[MessageType], symbols: &[String], subscribe: bool, _configs: Option<&HashMap>, ) -> Vec { msg_types .iter() .map(|msg_type| msg_type_to_channel(*msg_type)) .flat_map(|channel| { symbols.iter().map(|symbol| channel_symbol_to_command(channel, symbol, subscribe)) }) .collect::>() } #[cfg(test)] mod tests { use super::*; #[test] fn single_msg_type_multiple_symbols() { let commands = get_ws_commands( &[MessageType::Trade], &["BTC/USD".to_string(), "BTC-PERP".to_string()], true, None, ); assert_eq!(commands.len(), 2); println!("{}", commands[0]); assert_eq!(r#"{"op":"subscribe","channel":"trades","market":"BTC/USD"}"#, commands[0]); assert_eq!(r#"{"op":"subscribe","channel":"trades","market":"BTC-PERP"}"#, commands[1]); } #[test] fn multiple_msg_types_single_symbol() { let commands = get_ws_commands( &[MessageType::Trade, MessageType::L2Event], &["BTC-PERP".to_string()], true, None, ); assert_eq!(commands.len(), 2); assert_eq!(r#"{"op":"subscribe","channel":"trades","market":"BTC-PERP"}"#, commands[0]); assert_eq!(r#"{"op":"subscribe","channel":"orderbook","market":"BTC-PERP"}"#, commands[1]); } } ================================================ FILE: crypto-msg-type/src/exchanges/huobi.rs ================================================ use std::collections::HashMap; use crate::MessageType; fn msg_type_symbol_to_topic( msg_type: MessageType, symbol: &str, configs: Option<&HashMap>, ) -> String { let is_spot = symbol.to_lowercase() == *symbol; let channel = match msg_type { MessageType::Trade => "trade.detail", MessageType::L2Event => { if is_spot { "mbp.20" } else { "depth.size_20.high_freq" } } MessageType::L2TopK => { if is_spot { "depth.step1" } else { "depth.step7" } } MessageType::BBO => "bbo", MessageType::Ticker => "detail", MessageType::Candlestick => "kline", _ => panic!("Unknown message type {msg_type}"), }; if msg_type == MessageType::Candlestick { format!("market.{}.kline.{}", symbol, configs.unwrap().get("interval").unwrap()) } else { format!("market.{symbol}.{channel}") } } fn topic_to_command(topic: &str, subscribe: bool) -> String { if topic.ends_with("depth.size_20.high_freq") { format!( r#"{{"{}": "{}","data_type":"incremental","id": "crypto-ws-client"}}"#, if subscribe { "sub" } else { "unsub" }, topic, ) } else { format!( r#"{{"{}":"{}","id":"crypto-ws-client"}}"#, if subscribe { "sub" } else { "unsub" }, topic, ) } } pub(crate) fn get_ws_commands( msg_types: &[MessageType], symbols: &[String], subscribe: bool, configs: Option<&HashMap>, ) -> Vec { msg_types .iter() .flat_map(|msg_type| { symbols.iter().map(|symbol| msg_type_symbol_to_topic(*msg_type, symbol, configs)) }) .map(|topic| topic_to_command(&topic, subscribe)) .collect() } #[cfg(test)] mod tests { use super::*; #[test] fn single_msg_type_multiple_symbols() { let commands = get_ws_commands( &[MessageType::Trade], &["BTC-USD".to_string(), "ETH-USD".to_string()], true, None, ); assert_eq!(commands.len(), 2); assert_eq!(r#"{"sub":"market.BTC-USD.trade.detail","id":"crypto-ws-client"}"#, commands[0]); assert_eq!(r#"{"sub":"market.ETH-USD.trade.detail","id":"crypto-ws-client"}"#, commands[1]); } #[test] fn multiple_msg_types_single_symbol() { let commands = get_ws_commands( &[MessageType::Trade, MessageType::L2Event], &["BTC-USD".to_string()], true, None, ); assert_eq!(commands.len(), 2); assert_eq!(r#"{"sub":"market.BTC-USD.trade.detail","id":"crypto-ws-client"}"#, commands[0]); assert_eq!( r#"{"sub": "market.BTC-USD.depth.size_20.high_freq","data_type":"incremental","id": "crypto-ws-client"}"#, commands[1] ); } #[test] fn candlestick() { let mut configs = HashMap::new(); configs.insert("interval".to_string(), "1m".to_string()); let commands = get_ws_commands( &[MessageType::Candlestick], &["BTC-USD".to_string(), "ETH-USD".to_string()], true, Some(&configs), ); assert_eq!(commands.len(), 2); assert_eq!(r#"{"sub":"market.BTC-USD.kline.1m","id":"crypto-ws-client"}"#, commands[0]); assert_eq!(r#"{"sub":"market.ETH-USD.kline.1m","id":"crypto-ws-client"}"#, commands[1]); } } ================================================ FILE: crypto-msg-type/src/exchanges/mod.rs ================================================ pub(super) mod binance; pub(super) mod bitfinex; pub(super) mod bitmex; pub(super) mod bybit; pub(super) mod deribit; pub(super) mod ftx; pub(super) mod huobi; pub(super) mod okex; pub(super) mod okx; ================================================ FILE: crypto-msg-type/src/exchanges/okex.rs ================================================ use std::collections::HashMap; use crate::MessageType; fn get_market_type(symbol: &str) -> &'static str { if symbol.ends_with("-SWAP") { "swap" } else { let c = symbol.matches('-').count(); if c == 1 { "spot" } else if c == 2 { let date = &symbol[(symbol.len() - 6)..]; debug_assert!(date.parse::().is_ok()); "futures" } else { debug_assert!(symbol.ends_with("-C") || symbol.ends_with("-P")); "option" } } } fn msg_type_to_channel(msg_type: MessageType) -> &'static str { match msg_type { MessageType::Trade => "trade", MessageType::L2Event => "depth_l2_tbt", MessageType::L2TopK => "depth5", MessageType::BBO => "ticker", MessageType::Ticker => "ticker", MessageType::Candlestick => "candle", _ => panic!("Unknown message type {msg_type}"), } } fn channel_symbol_to_topic( channel: &str, symbol: &str, configs: Option<&HashMap>, ) -> String { let market_type = get_market_type(symbol); if channel == "candle" { format!("{}/candle{}s:{}", market_type, configs.unwrap().get("interval").unwrap(), symbol) } else { format!("{market_type}/{channel}:{symbol}") } } fn topics_to_command(topics: &[String], subscribe: bool) -> String { format!( r#"{{"op":"{}","args":{}}}"#, if subscribe { "subscribe" } else { "unsubscribe" }, serde_json::to_string(topics).unwrap() ) } pub(crate) fn get_ws_commands( msg_types: &[MessageType], symbols: &[String], subscribe: bool, configs: Option<&HashMap>, ) -> Vec { let topics = msg_types .iter() .map(|msg_type| msg_type_to_channel(*msg_type)) .flat_map(|channel| { symbols.iter().map(|symbol| channel_symbol_to_topic(channel, symbol, configs)) }) .collect::>(); vec![topics_to_command(&topics, subscribe)] } #[cfg(test)] mod tests { use super::*; #[test] fn single_msg_type_multiple_symbols() { let commands = get_ws_commands( &[MessageType::Trade], &["BTC-USDT-SWAP".to_string(), "ETH-USDT-SWAP".to_string()], true, None, ); assert_eq!(commands.len(), 1); assert_eq!( r#"{"op":"subscribe","args":["swap/trade:BTC-USDT-SWAP","swap/trade:ETH-USDT-SWAP"]}"#, commands[0] ); } #[test] fn multiple_msg_types_single_symbol() { let commands = get_ws_commands( &[MessageType::Trade, MessageType::L2Event], &["BTC-USDT-SWAP".to_string()], true, None, ); assert_eq!(commands.len(), 1); assert_eq!( r#"{"op":"subscribe","args":["swap/trade:BTC-USDT-SWAP","swap/depth_l2_tbt:BTC-USDT-SWAP"]}"#, commands[0] ); } #[test] fn candlestick() { let mut configs = HashMap::new(); configs.insert("interval".to_string(), "60".to_string()); let commands = get_ws_commands( &[MessageType::Candlestick], &["BTC-USDT-SWAP".to_string(), "ETH-USDT-SWAP".to_string()], true, Some(&configs), ); assert_eq!(commands.len(), 1); assert_eq!( r#"{"op":"subscribe","args":["swap/candle60s:BTC-USDT-SWAP","swap/candle60s:ETH-USDT-SWAP"]}"#, commands[0] ); } } ================================================ FILE: crypto-msg-type/src/exchanges/okx.rs ================================================ use std::collections::{BTreeMap, HashMap}; use crate::MessageType; fn msg_type_to_channel(msg_type: MessageType) -> &'static str { match msg_type { MessageType::Trade => "trades", MessageType::L2Event => "books-l2-tbt", MessageType::L2TopK => "books5", MessageType::Ticker => "tickers", MessageType::Candlestick => "candle", _ => panic!("Unknown message type {msg_type}"), } } fn channel_symbol_to_topic( channel: &str, symbol: &str, configs: Option<&HashMap>, ) -> String { if channel == "candle" { format!("candle{}:{}", configs.unwrap().get("interval").unwrap(), symbol) } else { format!("{channel}:{symbol}") } } fn topics_to_command(topics: &[String], subscribe: bool) -> String { let arr = topics .iter() .map(|s| { let mut map = BTreeMap::new(); let v: Vec<&str> = s.split(':').collect(); let channel = v[0]; let symbol = v[1]; map.insert("channel".to_string(), channel.to_string()); map.insert("instId".to_string(), symbol.to_string()); map }) .collect::>>(); format!( r#"{{"op":"{}","args":{}}}"#, if subscribe { "subscribe" } else { "unsubscribe" }, serde_json::to_string(&arr).unwrap(), ) } pub(crate) fn get_ws_commands( msg_types: &[MessageType], symbols: &[String], subscribe: bool, configs: Option<&HashMap>, ) -> Vec { let topics = msg_types .iter() .map(|msg_type| msg_type_to_channel(*msg_type)) .flat_map(|channel| { symbols.iter().map(|symbol| channel_symbol_to_topic(channel, symbol, configs)) }) .collect::>(); vec![topics_to_command(&topics, subscribe)] } #[cfg(test)] mod tests { use super::*; #[test] fn single_msg_type_multiple_symbols() { let commands = get_ws_commands( &[MessageType::Trade], &["BTC-USDT-SWAP".to_string(), "ETH-USDT-SWAP".to_string()], true, None, ); assert_eq!(commands.len(), 1); assert_eq!( r#"{"op":"subscribe","args":[{"channel":"trades","instId":"BTC-USDT-SWAP"},{"channel":"trades","instId":"ETH-USDT-SWAP"}]}"#, commands[0] ); } #[test] fn multiple_msg_types_single_symbol() { let commands = get_ws_commands( &[MessageType::Trade, MessageType::L2Event], &["BTC-USDT-SWAP".to_string()], true, None, ); assert_eq!(commands.len(), 1); assert_eq!( r#"{"op":"subscribe","args":[{"channel":"trades","instId":"BTC-USDT-SWAP"},{"channel":"books-l2-tbt","instId":"BTC-USDT-SWAP"}]}"#, commands[0] ); } #[test] fn candlestick() { let mut configs = HashMap::new(); configs.insert("interval".to_string(), "1m".to_string()); let commands = get_ws_commands( &[MessageType::Candlestick], &["BTC-USDT-SWAP".to_string(), "ETH-USDT-SWAP".to_string()], true, Some(&configs), ); assert_eq!(commands.len(), 1); assert_eq!( r#"{"op":"subscribe","args":[{"channel":"candle1m","instId":"BTC-USDT-SWAP"},{"channel":"candle1m","instId":"ETH-USDT-SWAP"}]}"#, commands[0] ); } } ================================================ FILE: crypto-msg-type/src/lib.rs ================================================ mod exchanges; use std::collections::HashMap; use serde::{Deserialize, Serialize}; use strum_macros::{Display, EnumString}; /// Crypto message types. /// /// L2Snapshot and L2TopK are very similar, the former is from RESTful API, /// the latter is from websocket. #[repr(C)] #[derive(Copy, Clone, Eq, PartialEq, Serialize, Deserialize, Display, Debug, EnumString, Hash)] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum MessageType { /// All other messages Other, /// tick-by-tick trade messages Trade, /// Incremental level2 orderbook updates L2Event, /// Level2 snapshot from RESTful API L2Snapshot, /// Level2 top K snapshots from websocket #[serde(rename = "l2_topk")] #[strum(serialize = "l2_topk")] L2TopK, /// Incremental level3 orderbook updates L3Event, /// Level3 snapshot from RESTful API L3Snapshot, /// Best bid and ask #[serde(rename = "bbo")] #[allow(clippy::upper_case_acronyms)] BBO, /// 24hr rolling window ticker Ticker, /// OHLCV candlestick Candlestick, /// Funding rate FundingRate, /// Open interest OpenInterest, /// Long/short ratio LongShortRatio, /// Taker buy/sell volume TakerVolume, } /// Translate to websocket subscribe/unsubscribe commands. /// /// `configs` Some `msg_type` requires a config, for example, /// `Candlestick` requires an `interval` parameter. pub fn get_ws_commands( exchange: &str, msg_types: &[MessageType], symbols: &[String], subscribe: bool, configs: Option<&HashMap>, ) -> Vec { if msg_types.is_empty() || symbols.is_empty() { return Vec::new(); } match exchange { "binance" => exchanges::binance::get_ws_commands(msg_types, symbols, subscribe, configs), "bitfinex" => exchanges::bitfinex::get_ws_commands(msg_types, symbols, subscribe, configs), "bitmex" => exchanges::bitmex::get_ws_commands(msg_types, symbols, subscribe, configs), "bybit" => exchanges::bybit::get_ws_commands(msg_types, symbols, subscribe, configs), "deribit" => exchanges::deribit::get_ws_commands(msg_types, symbols, subscribe, configs), "ftx" => exchanges::ftx::get_ws_commands(msg_types, symbols, subscribe, configs), "huobi" => exchanges::huobi::get_ws_commands(msg_types, symbols, subscribe, configs), "okex" => exchanges::okex::get_ws_commands(msg_types, symbols, subscribe, configs), "okx" => exchanges::okx::get_ws_commands(msg_types, symbols, subscribe, configs), _ => Vec::new(), } } ================================================ FILE: crypto-rest-client/Cargo.toml ================================================ [package] name = "crypto-rest-client" version = "1.0.1" authors = ["soulmachine "] edition = "2021" description = "An RESTful client for all cryptocurrency exchanges." license = "Apache-2.0" repository = "https://github.com/crypto-crawler/crypto-crawler-rs/tree/main/crypto-rest-client" keywords = ["cryptocurrency", "blockchain", "trading"] [dependencies] crypto-market-type = "1.1.5" once_cell = "1.17.1" log = "0.4.17" regex = "1.7.1" reqwest = { version = "0.11.14", features = ["blocking", "gzip", "socks"] } serde = { version = "1.0.152", features = ["derive"] } serde_json = "1.0.93" [dev_dependencies] test-case = "1" ================================================ FILE: crypto-rest-client/README.md ================================================ # crypto-rest-client An RESTful client for all cryptocurrency exchanges. ## Example ```rust use crypto_rest_client::{BinanceClient}; fn main() { let config: HashMap<&str, &str> = vec![ ("api_key", "your-API-key"), ("api_secret", "your-API-secret"), ].into_iter().collect(); let rest_client = BinanceClient::new(config); // buy let transaction_id = rest_client.place_order("Spot", "btcusdt", 27999.9, 5.0, false); println!("{}", transactionId); } ``` ## Supported Exchanges - Binance - Huobi - OKEx ================================================ FILE: crypto-rest-client/src/error.rs ================================================ use std::{error::Error as StdError, fmt}; pub(crate) type Result = std::result::Result; #[derive(Debug)] pub struct Error(pub String); impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.0) } } impl StdError for Error {} impl From for Error { fn from(err: reqwest::Error) -> Self { Error(err.to_string()) } } impl From for Error { fn from(err: serde_json::Error) -> Self { Error(err.to_string()) } } ================================================ FILE: crypto-rest-client/src/exchanges/binance/binance_inverse.rs ================================================ use super::{super::utils::http_get, utils::*}; use crate::error::Result; use std::collections::BTreeMap; const BASE_URL: &str = "https://dapi.binance.com"; /// Binance Coin-margined Future and Swap market /// /// * REST API doc: /// * Trading at: /// Rate Limits: /// * 2400 request weight per minute pub struct BinanceInverseRestClient { _api_key: Option, _api_secret: Option, } impl BinanceInverseRestClient { pub fn new(api_key: Option, api_secret: Option) -> Self { BinanceInverseRestClient { _api_key: api_key, _api_secret: api_secret } } /// Get compressed, aggregate trades. /// /// Equivalent to `/dapi/v1/aggTrades` with `limit=1000` /// /// For example: /// /// - /// - pub fn fetch_agg_trades( symbol: &str, from_id: Option, start_time: Option, end_time: Option, ) -> Result { check_symbol(symbol); let symbol = Some(symbol); let limit = Some(1000); gen_api_binance!("/dapi/v1/aggTrades", symbol, from_id, start_time, end_time, limit) } /// Get a Level2 snapshot of orderbook. /// /// Equivalent to `/dapi/v1/depth` with `limit=1000` /// /// For example: /// /// - /// - pub fn fetch_l2_snapshot(symbol: &str) -> Result { check_symbol(symbol); let symbol = Some(symbol); let limit = Some(1000); gen_api_binance!("/dapi/v1/depth", symbol, limit) } /// Get open interest. /// /// For example: /// /// - /// - pub fn fetch_open_interest(symbol: &str) -> Result { check_symbol(symbol); let symbol = Some(symbol); gen_api_binance!("/dapi/v1/openInterest", symbol) } } ================================================ FILE: crypto-rest-client/src/exchanges/binance/binance_linear.rs ================================================ use super::{super::utils::http_get, utils::*}; use crate::error::Result; use std::collections::BTreeMap; const BASE_URL: &str = "https://fapi.binance.com"; /// Binance USDT-margined Future and Swap market. /// /// * REST API doc: /// * Trading at: /// * Rate Limits: /// * 2400 request weight per minute pub struct BinanceLinearRestClient { _api_key: Option, _api_secret: Option, } impl BinanceLinearRestClient { pub fn new(api_key: Option, api_secret: Option) -> Self { BinanceLinearRestClient { _api_key: api_key, _api_secret: api_secret } } /// Get compressed, aggregate trades. /// /// Equivalent to `/fapi/v1/aggTrades` with `limit=1000` /// /// For example: /// /// - /// - pub fn fetch_agg_trades( symbol: &str, from_id: Option, start_time: Option, end_time: Option, ) -> Result { check_symbol(symbol); let symbol = Some(symbol); let limit = Some(1000); gen_api_binance!("/fapi/v1/aggTrades", symbol, from_id, start_time, end_time, limit) } /// Get a Level2 snapshot of orderbook. /// /// Equivalent to `/fapi/v1/depth` with `limit=1000` /// /// For example: /// /// - /// - pub fn fetch_l2_snapshot(symbol: &str) -> Result { check_symbol(symbol); let symbol = Some(symbol); let limit = Some(1000); gen_api_binance!("/fapi/v1/depth", symbol, limit) } /// Get open interest. /// /// For example: /// /// - /// - pub fn fetch_open_interest(symbol: &str) -> Result { check_symbol(symbol); let symbol = Some(symbol); gen_api_binance!("/fapi/v1/openInterest", symbol) } } ================================================ FILE: crypto-rest-client/src/exchanges/binance/binance_option.rs ================================================ use super::{super::utils::http_get, utils::*}; use crate::error::Result; use std::collections::BTreeMap; const BASE_URL: &str = "https://vapi.binance.com"; /// Binance Option market. /// /// * REST API doc: /// * Trading at: pub struct BinanceOptionRestClient { _api_key: Option, _api_secret: Option, } impl BinanceOptionRestClient { pub fn new(api_key: Option, api_secret: Option) -> Self { BinanceOptionRestClient { _api_key: api_key, _api_secret: api_secret } } /// Get most recent trades. /// /// 500 recent trades are returned. /// /// For example: pub fn fetch_trades(symbol: &str, start_time: Option) -> Result { check_symbol(symbol); let t = start_time; gen_api_binance!(format!("/vapi/v1/trades?symbol={symbol}&limit=500"), t) } /// Get a Level2 snapshot of orderbook. /// /// For example: pub fn fetch_l2_snapshot(symbol: &str) -> Result { check_symbol(symbol); let symbol = Some(symbol); let limit = Some(1000); gen_api_binance!("/vapi/v1/depth", symbol, limit) } } ================================================ FILE: crypto-rest-client/src/exchanges/binance/binance_spot.rs ================================================ use super::{super::utils::http_get, utils::*}; use crate::error::Result; use std::collections::BTreeMap; const BASE_URL: &str = "https://api.binance.com"; /// Binance Spot market. /// /// * RESTful API doc: /// * Trading at: /// * Rate Limits: /// * 1200 request weight per minute /// * 6100 raw requests per 5 minutes pub struct BinanceSpotRestClient { _api_key: Option, _api_secret: Option, } impl BinanceSpotRestClient { pub fn new(api_key: Option, api_secret: Option) -> Self { BinanceSpotRestClient { _api_key: api_key, _api_secret: api_secret } } /// Get compressed, aggregate trades. /// /// Equivalent to `/api/v3/aggTrades` with `limit=1000` /// /// For example: pub fn fetch_agg_trades( symbol: &str, from_id: Option, start_time: Option, end_time: Option, ) -> Result { check_symbol(symbol); let symbol = Some(symbol); let limit = Some(1000); gen_api_binance!("/api/v3/aggTrades", symbol, from_id, start_time, end_time, limit) } /// Get a Level2 snapshot of orderbook. /// /// Equivalent to `/api/v3/depth` with `limit=1000` /// /// For example: pub fn fetch_l2_snapshot(symbol: &str) -> Result { check_symbol(symbol); let symbol = Some(symbol); let limit = Some(1000); gen_api_binance!("/api/v3/depth", symbol, limit) } } ================================================ FILE: crypto-rest-client/src/exchanges/binance/mod.rs ================================================ #[macro_use] mod utils; pub(crate) mod binance_inverse; pub(crate) mod binance_linear; pub(crate) mod binance_option; pub(crate) mod binance_spot; use crate::error::Result; use crypto_market_type::MarketType; pub(crate) fn fetch_l2_snapshot(market_type: MarketType, symbol: &str) -> Result { let func = match market_type { MarketType::Spot => binance_spot::BinanceSpotRestClient::fetch_l2_snapshot, MarketType::InverseFuture | MarketType::InverseSwap => { binance_inverse::BinanceInverseRestClient::fetch_l2_snapshot } MarketType::LinearFuture | MarketType::LinearSwap => { binance_linear::BinanceLinearRestClient::fetch_l2_snapshot } MarketType::EuropeanOption => binance_option::BinanceOptionRestClient::fetch_l2_snapshot, _ => panic!("Binance unknown market_type: {market_type}"), }; func(symbol) } pub(crate) fn fetch_open_interest(market_type: MarketType, symbol: &str) -> Result { let func = match market_type { MarketType::InverseFuture | MarketType::InverseSwap => { binance_inverse::BinanceInverseRestClient::fetch_open_interest } MarketType::LinearFuture | MarketType::LinearSwap => { binance_linear::BinanceLinearRestClient::fetch_open_interest } _ => panic!("Binance {market_type} does not have open interest data"), }; func(symbol) } ================================================ FILE: crypto-rest-client/src/exchanges/binance/utils.rs ================================================ use std::collections::BTreeMap; use crate::error::{Error, Result}; use once_cell::sync::Lazy; use regex::Regex; use serde_json::Value; static SYMBOL_PATTERN: Lazy = Lazy::new(|| Regex::new("^[A-Z0-9-_.]{1,20}$").unwrap()); pub(super) fn check_symbol(symbol: &str) { if !SYMBOL_PATTERN.is_match(symbol) { panic!("Illegal symbol {symbol}, legal symbol should be '^[A-Z0-9-_.]{{1,20}}$'."); } } pub(super) fn check_code_in_body(resp: String) -> Result { let obj = serde_json::from_str::>(&resp); if obj.is_err() { return Ok(resp); } match obj.unwrap().get("code") { Some(code) => { if code.as_i64().unwrap() != 0 { Err(Error(resp)) } else { Ok(resp) } } None => Ok(resp), } } macro_rules! gen_api_binance { ( $path:expr$(, $param_name:ident )* ) => { { #[allow(unused_mut)] let mut params = BTreeMap::new(); $( if let Some(param_name) = $param_name { params.insert(stringify!($param_name).to_string(), param_name.to_string()); } )* let ret = http_get(format!("{}{}",BASE_URL, $path).as_str(), ¶ms); match ret { Ok(resp) => check_code_in_body(resp), Err(_) => ret, } } } } ================================================ FILE: crypto-rest-client/src/exchanges/bitfinex.rs ================================================ use super::utils::http_get; use crate::error::Result; use std::collections::BTreeMap; const BASE_URL: &str = "https://api-pub.bitfinex.com"; /// The REST client for Bitfinex, including all markets. /// /// * REST API doc: /// * Spot: /// * Swap: /// * Funding: pub struct BitfinexRestClient { _api_key: Option, _api_secret: Option, } impl BitfinexRestClient { pub fn new(api_key: Option, api_secret: Option) -> Self { BitfinexRestClient { _api_key: api_key, _api_secret: api_secret } } /// /v2/trades/Symbol/hist pub fn fetch_trades( symbol: &str, limit: Option, start: Option, end: Option, sort: Option, ) -> Result { gen_api!(format!("/v2/trades/{symbol}/hist"), limit, start, end, sort) } /// Get a Level2 snapshot of orderbook. /// /// Equivalent to `/v2/book/Symbol/P0` with `len=100` /// /// For example: /// /// Ratelimit: 90 req/min pub fn fetch_l2_snapshot(symbol: &str) -> Result { let len = Some(100); gen_api!(format!("/v2/book/{symbol}/P0"), len) } /// Get a Level3 snapshot of orderbook. /// /// Equivalent to `/v2/book/Symbol/R0` with `len=100` /// /// For example: pub fn fetch_l3_snapshot(symbol: &str) -> Result { let len = Some(100); gen_api!(format!("/v2/book/{symbol}/R0"), len) } } ================================================ FILE: crypto-rest-client/src/exchanges/bitget/bitget_spot.rs ================================================ use super::super::utils::http_get; use crate::error::Result; use std::collections::BTreeMap; const BASE_URL: &str = "https://api.bitget.com"; /// The RESTful client for Bitget spot market. /// /// * RESTful API doc: /// * Trading at: pub struct BitgetSpotRestClient { _api_key: Option, _api_secret: Option, } impl BitgetSpotRestClient { pub fn new(api_key: Option, api_secret: Option) -> Self { BitgetSpotRestClient { _api_key: api_key, _api_secret: api_secret } } /// Get the latest Level2 snapshot of orderbook. /// /// Top 150 bids and asks are returned. /// /// For example: , pub fn fetch_l2_snapshot(symbol: &str) -> Result { gen_api!(format!("/api/spot/v1/market/depth?symbol={symbol}&type=step0")) } } ================================================ FILE: crypto-rest-client/src/exchanges/bitget/bitget_swap.rs ================================================ use super::super::utils::http_get; use crate::error::Result; use std::collections::BTreeMap; const BASE_URL: &str = "https://api.bitget.com"; /// The RESTful client for Bitget swap markets. /// /// * RESTful API doc: /// * Trading at: pub struct BitgetSwapRestClient { _api_key: Option, _api_secret: Option, } impl BitgetSwapRestClient { pub fn new(api_key: Option, api_secret: Option) -> Self { BitgetSwapRestClient { _api_key: api_key, _api_secret: api_secret } } /// Get the latest Level2 snapshot of orderbook. /// /// For example: /// /// Rate Limit:20 requests per 2 seconds pub fn fetch_l2_snapshot(symbol: &str) -> Result { gen_api!(format!("/api/mix/v1/market/depth?symbol={symbol}&limit=100")) } /// Get open interest. /// /// For example: /// /// - pub fn fetch_open_interest(symbol: &str) -> Result { gen_api!(format!("/api/mix/v1/market/open-interest?symbol={symbol}")) } } ================================================ FILE: crypto-rest-client/src/exchanges/bitget/mod.rs ================================================ mod bitget_spot; mod bitget_swap; pub use bitget_spot::BitgetSpotRestClient; pub use bitget_swap::BitgetSwapRestClient; use crate::error::Result; use crypto_market_type::MarketType; pub(crate) fn fetch_l2_snapshot(market_type: MarketType, symbol: &str) -> Result { let func = match market_type { MarketType::Spot => bitget_spot::BitgetSpotRestClient::fetch_l2_snapshot, MarketType::InverseFuture | MarketType::InverseSwap | MarketType::LinearSwap => { bitget_swap::BitgetSwapRestClient::fetch_l2_snapshot } _ => panic!("Bitget unknown market_type: {market_type}"), }; func(symbol) } pub(crate) fn fetch_open_interest(market_type: MarketType, symbol: &str) -> Result { let func = match market_type { MarketType::InverseFuture | MarketType::InverseSwap | MarketType::LinearSwap => { bitget_swap::BitgetSwapRestClient::fetch_open_interest } _ => panic!("Bitget {market_type} does not have open interest"), }; func(symbol) } ================================================ FILE: crypto-rest-client/src/exchanges/bithumb.rs ================================================ use super::utils::http_get; use crate::error::Result; use std::collections::BTreeMap; const BASE_URL: &str = "https://global-openapi.bithumb.pro/openapi/v1"; /// The REST client for Bithumb. /// /// Bithumb has only Spot market. /// /// * REST API doc: /// * Trading at: /// * Rate Limits: /// * 135 requests per 1 second for public APIs. /// * 15 requests per 1 second for private APIs. pub struct BithumbRestClient { _api_key: Option, _api_secret: Option, } impl BithumbRestClient { pub fn new(api_key: Option, api_secret: Option) -> Self { BithumbRestClient { _api_key: api_key, _api_secret: api_secret } } /// Get most recent trades. /// /// For example: pub fn fetch_trades(symbol: &str) -> Result { gen_api!(format!("/spot/trades?symbol={symbol}")) } /// Get the latest Level2 orderbook snapshot. /// /// For example: pub fn fetch_l2_snapshot(symbol: &str) -> Result { gen_api!(format!("/spot/orderBook?symbol={symbol}")) } } ================================================ FILE: crypto-rest-client/src/exchanges/bitmex.rs ================================================ use super::utils::http_get; use crate::error::Result; use std::collections::BTreeMap; const BASE_URL: &str = "https://www.bitmex.com/api/v1"; /// The REST client for BitMEX. /// /// BitMEX has Swap and Future markets. /// /// * REST API doc: /// * Trading at: /// * Rate Limits: /// * 60 requests per minute on all routes (reduced to 30 when /// unauthenticated) /// * 10 requests per second on certain routes (see below) pub struct BitmexRestClient { _api_key: Option, _api_secret: Option, } impl BitmexRestClient { pub fn new(api_key: Option, api_secret: Option) -> Self { BitmexRestClient { _api_key: api_key, _api_secret: api_secret } } /// Get trades from a beginning time. /// /// Equivalent to `/trade` with `count=1000` /// /// For example: #[allow(non_snake_case)] pub fn fetch_trades(symbol: &str, start_time: Option) -> Result { let symbol = Some(symbol); let startTime = start_time; gen_api!("/trade", symbol, startTime) } /// Get a full Level2 snapshot of orderbook. /// /// Equivalent to `/orderBook/L2` with `depth=0` /// /// For example: pub fn fetch_l2_snapshot(symbol: &str) -> Result { let symbol = Some(symbol); let depth = Some(0); gen_api!("/orderBook/L2", symbol, depth) } } ================================================ FILE: crypto-rest-client/src/exchanges/bitstamp.rs ================================================ use super::utils::http_get; use crate::error::Result; use std::collections::BTreeMap; const BASE_URL: &str = "https://www.bitstamp.net/api"; /// The REST client for Bitstamp. /// /// Bitstamp has only Spot market. /// /// * REST API doc: /// * Trading at: /// * Rate Limits: /// * Do not make more than 8000 requests per 10 minutes or we will ban your /// IP address. pub struct BitstampRestClient { _api_key: Option, _api_secret: Option, } impl BitstampRestClient { pub fn new(api_key: Option, api_secret: Option) -> Self { BitstampRestClient { _api_key: api_key, _api_secret: api_secret } } /// Get trades. /// /// `/v2/transactions/{symbol}/` /// /// `time` specifies the time interval from which we want the transactions /// to be returned. Possible values are "minute", "hour" (default) or "day". /// /// For example: pub fn fetch_trades(symbol: &str, time: Option) -> Result { gen_api!(format!("/v2/transactions/{symbol}/"), time) } /// Get a full Level2 orderbook snapshot. /// /// /// Equivalent to `/order_book/symbol` with `group=1` /// /// For example: pub fn fetch_l2_snapshot(symbol: &str) -> Result { gen_api!(format!("/v2/order_book/{symbol}")) } /// Get a full Level3 orderbook snapshot. /// /// Equivalent to `/order_book/symbol` with `group=2` /// /// For example: pub fn fetch_l3_snapshot(symbol: &str) -> Result { gen_api!(format!("/v2/order_book/{symbol}?group=2")) } } ================================================ FILE: crypto-rest-client/src/exchanges/bitz/bitz_spot.rs ================================================ use super::super::utils::http_get; use crate::error::Result; use std::collections::BTreeMap; const BASE_URL: &str = "https://apiv2.bitz.com"; /// The RESTful client for BitZ spot market. /// /// * RESTful API doc: /// * Trading at: /// * Rate Limits: /// * no more than 30 times within 1 sec pub struct BitzSpotRestClient { _api_key: Option, _api_secret: Option, } impl BitzSpotRestClient { pub fn new(api_key: Option, api_secret: Option) -> Self { BitzSpotRestClient { _api_key: api_key, _api_secret: api_secret } } /// Get the latest Level2 snapshot of orderbook. /// /// For example: , pub fn fetch_l2_snapshot(symbol: &str) -> Result { gen_api!(format!("/V2/Market/depth?symbol={symbol}")) } } ================================================ FILE: crypto-rest-client/src/exchanges/bitz/bitz_swap.rs ================================================ use super::super::utils::http_get; use crate::error::{Error, Result}; use std::collections::{BTreeMap, HashMap}; use serde::{Deserialize, Serialize}; use serde_json::Value; const BASE_URL: &str = "https://apiv2.bitz.com"; /// The RESTful client for BitZ swap markets. /// /// * RESTful API doc: /// * Trading at: /// * Rate Limits: /// * no more than 30 times within 1 sec pub struct BitzSwapRestClient { _api_key: Option, _api_secret: Option, } impl BitzSwapRestClient { pub fn new(api_key: Option, api_secret: Option) -> Self { BitzSwapRestClient { _api_key: api_key, _api_secret: api_secret } } /// Get the latest Level2 snapshot of orderbook. /// /// Top 100 bids and asks are returned. /// /// For example: , pub fn fetch_l2_snapshot(symbol: &str) -> Result { let symbol_id_map = get_symbol_id_map()?; if !symbol_id_map.contains_key(symbol) { return Err(Error(format!("Can NOT find contractId for the pair {symbol}"))); } let contract_id = symbol_id_map.get(symbol).unwrap(); gen_api!(format!("/V2/Market/getContractOrderBook?contractId={contract_id}&depth=100")) } /// Get open interest. /// /// For example: pub fn fetch_open_interest(symbol: Option<&str>) -> Result { if let Some(symbol) = symbol { let symbol_id_map = get_symbol_id_map()?; if !symbol_id_map.contains_key(symbol) { return Err(Error(format!("Can NOT find contractId for the pair {symbol}"))); } let contract_id = symbol_id_map.get(symbol).unwrap(); gen_api!(format!("/V2/Market/getContractTickers?contractId={contract_id}")) } else { gen_api!("/V2/Market/getContractTickers") } } } #[derive(Clone, Serialize, Deserialize)] #[allow(non_snake_case)] struct SwapMarket { contractId: String, // contract id pair: String, //contract market status: String, #[serde(flatten)] extra: HashMap, } #[derive(Serialize, Deserialize)] struct Response { status: i64, msg: String, data: Vec, time: i64, microtime: String, source: String, } fn get_symbol_id_map() -> Result> { let params = BTreeMap::new(); let txt = http_get("https://apiv2.bitz.com/Market/getContractCoin", ¶ms)?; let resp = serde_json::from_str::(&txt)?; if resp.status != 200 { return Err(Error(txt)); } let mut symbol_id_map = HashMap::::new(); for x in resp.data.iter() { if x.status == "1" { symbol_id_map.insert(x.pair.clone(), x.contractId.clone()); } } Ok(symbol_id_map) } ================================================ FILE: crypto-rest-client/src/exchanges/bitz/mod.rs ================================================ mod bitz_spot; mod bitz_swap; pub use bitz_spot::BitzSpotRestClient; pub use bitz_swap::BitzSwapRestClient; use crate::error::Result; use crypto_market_type::MarketType; pub(crate) fn fetch_l2_snapshot(market_type: MarketType, symbol: &str) -> Result { let func = match market_type { MarketType::Spot => bitz_spot::BitzSpotRestClient::fetch_l2_snapshot, MarketType::InverseSwap | MarketType::LinearSwap => { bitz_swap::BitzSwapRestClient::fetch_l2_snapshot } _ => panic!("BitZ unknown market_type: {market_type}"), }; func(symbol) } pub(crate) fn fetch_open_interest(market_type: MarketType, symbol: Option<&str>) -> Result { let func = match market_type { MarketType::LinearSwap | MarketType::InverseSwap => { bitz_swap::BitzSwapRestClient::fetch_open_interest } _ => panic!("bitz {market_type} does not have open interest"), }; func(symbol) } ================================================ FILE: crypto-rest-client/src/exchanges/bybit.rs ================================================ use super::utils::http_get; use crate::error::Result; use std::collections::BTreeMap; const BASE_URL: &str = "https://api.bybit.com/v2"; /// The RESTful client for Bybit. /// /// Bybit has InverseSwap and LinearSwap markets. /// /// * RESTful API doc: /// * Trading at: /// * InverseSwap /// * LinearSwap /// * Rate Limit: /// * GET method: /// * 50 requests per second continuously for 2 minutes /// * 70 requests per second continuously for 5 seconds /// * POST method: /// * 20 requests per second continuously for 2 minutes /// * 50 requests per second continuously for 5 seconds pub struct BybitRestClient { _api_key: Option, _api_secret: Option, } impl BybitRestClient { pub fn new(api_key: Option, api_secret: Option) -> Self { BybitRestClient { _api_key: api_key, _api_secret: api_secret } } /// Get the latest Level2 snapshot of orderbook. /// /// Top 50 bids and asks are returned. /// /// For example: , pub fn fetch_l2_snapshot(symbol: &str) -> Result { gen_api!(format!("/public/orderBook/L2?symbol={symbol}")) } /// Get open interest. /// /// For example: /// /// - /// - /// - pub fn fetch_open_interest(symbol: &str) -> Result { gen_api!(format!("/public/open-interest?symbol={symbol}&period=5min&limit=200")) } /// Get long-short ratio. /// /// For example: /// /// - /// - /// - pub fn fetch_long_short_ratio(symbol: &str) -> Result { gen_api!(format!("/public/account-ratio?symbol={symbol}&period=5min&limit=200")) } } ================================================ FILE: crypto-rest-client/src/exchanges/coinbase_pro.rs ================================================ use super::utils::http_get; use crate::error::Result; use std::collections::BTreeMap; const BASE_URL: &str = "https://api.exchange.coinbase.com"; /// The REST client for CoinbasePro. /// /// CoinbasePro has only Spot market. /// /// * REST API doc: /// * Trading at: /// * Rate Limits: /// * This endpoint has a custom rate limit by profile ID: 25 requests per /// second, up to 50 requests per second in bursts pub struct CoinbaseProRestClient { _api_key: Option, _api_secret: Option, } impl CoinbaseProRestClient { pub fn new(api_key: Option, api_secret: Option) -> Self { CoinbaseProRestClient { _api_key: api_key, _api_secret: api_secret } } /// List the latest trades for a product. /// /// `/products/{symbol}/trades` /// /// For example: pub fn fetch_trades(symbol: &str) -> Result { gen_api!(format!("/products/{symbol}/trades")) } /// Get the latest Level2 orderbook snapshot. /// /// Top 50 bids and asks (aggregated) are returned. /// /// For example: pub fn fetch_l2_snapshot(symbol: &str) -> Result { gen_api!(format!("/products/{symbol}/book?level=2")) } /// Get the latest Level3 orderbook snapshot. /// /// Full order book (non aggregated) are returned. /// /// For example: pub fn fetch_l3_snapshot(symbol: &str) -> Result { gen_api!(format!("/products/{symbol}/book?level=3")) } } ================================================ FILE: crypto-rest-client/src/exchanges/deribit.rs ================================================ use super::utils::http_get; use crate::error::Result; use std::collections::BTreeMap; const BASE_URL: &str = "https://www.deribit.com/api/v2"; /// The RESTful client for Deribit. /// /// Deribit has InverseFuture, InverseSwap and Option markets. /// /// * WebSocket API doc: /// * Trading at: /// * Future /// * Option /// * Rate Limits: /// * Each sub-account has a rate limit of 20 requests per second pub struct DeribitRestClient { _api_key: Option, _api_secret: Option, } impl DeribitRestClient { pub fn new(api_key: Option, api_secret: Option) -> Self { DeribitRestClient { _api_key: api_key, _api_secret: api_secret } } /// Get most recent trades. /// /// 100 trades are returned. /// /// For example: pub fn fetch_trades(symbol: &str) -> Result { gen_api!(format!( "/public/get_last_trades_by_instrument?count=100&instrument_name={symbol}" )) } /// Get the latest Level2 snapshot of orderbook. /// /// Top 2000 bids and asks are returned. /// /// For example: , pub fn fetch_l2_snapshot(symbol: &str) -> Result { gen_api!(format!("/public/get_order_book?depth=2000&instrument_name={symbol}",)) } /// Get open interest. /// /// For example: /// - /// - pub fn fetch_open_interest(symbol: Option<&str>) -> Result { if let Some(symbol) = symbol { gen_api!(format!("/public/get_book_summary_by_instrument?instrument_name={symbol}")) } else { let btc = gen_api!("/public/get_book_summary_by_currency?currency=BTC")?; let eth = gen_api!("/public/get_book_summary_by_currency?currency=ETH")?; let sol = gen_api!("/public/get_book_summary_by_currency?currency=SOL")?; let usdc = gen_api!("/public/get_book_summary_by_currency?currency=USDC")?; Ok(format!("{btc}\n{eth}\n{sol}\n{usdc}")) } } } ================================================ FILE: crypto-rest-client/src/exchanges/dydx/dydx_swap.rs ================================================ use super::super::utils::http_get; use crate::error::Result; use std::collections::BTreeMap; const BASE_URL: &str = "https://api.dydx.exchange"; /// dYdX perpetual RESTful client. /// /// * REST API doc: /// * Trading at: /// * Rate Limits: /// * 100 requests per 10 seconds pub struct DydxSwapRestClient { _api_key: Option, _api_secret: Option, } impl DydxSwapRestClient { pub fn new(api_key: Option, api_secret: Option) -> Self { DydxSwapRestClient { _api_key: api_key, _api_secret: api_secret } } /// Get a Level2 orderbook snapshot. /// /// All price levels are returned. /// /// For example: pub fn fetch_l2_snapshot(symbol: &str) -> Result { gen_api!(format!("/v3/orderbook/{symbol}")) } /// Get open interest. /// /// For example: pub fn fetch_open_interest() -> Result { gen_api!("/v3/markets") } } ================================================ FILE: crypto-rest-client/src/exchanges/dydx/mod.rs ================================================ pub(crate) mod dydx_swap; use crate::error::Result; use crypto_market_type::MarketType; pub(crate) fn fetch_l2_snapshot(market_type: MarketType, symbol: &str) -> Result { let func = match market_type { MarketType::LinearSwap => dydx_swap::DydxSwapRestClient::fetch_l2_snapshot, _ => panic!("dYdX does not have the {market_type} market type"), }; func(symbol) } pub(crate) fn fetch_open_interest(market_type: MarketType) -> Result { match market_type { MarketType::InverseSwap | MarketType::LinearSwap => { dydx_swap::DydxSwapRestClient::fetch_open_interest() } _ => panic!("dYdX {market_type} does not have open interest"), } } ================================================ FILE: crypto-rest-client/src/exchanges/ftx.rs ================================================ use super::utils::http_get; use crate::error::Result; use std::collections::BTreeMap; const BASE_URL: &str = "https://ftx.com/api"; /// The RESTful client for FTX. /// /// FTX has Spot, LinearFuture, LinearSwap, Option, Move and BVOL markets. /// /// * RESTful API doc: /// * Trading at /// * Rate Limits: /// * Non-order placement requests do not count towards rate limits. /// * Rate limits are tiered by account trading volumes. pub struct FtxRestClient { _api_key: Option, _api_secret: Option, } impl FtxRestClient { pub fn new(api_key: Option, api_secret: Option) -> Self { FtxRestClient { _api_key: api_key, _api_secret: api_secret } } /// Get the latest Level2 snapshot of orderbook. /// /// Top 100 bids and asks are returned. /// /// For example: , // pub fn fetch_l2_snapshot(symbol: &str) -> Result { gen_api!(format!("/markets/{symbol}/orderbook?depth=100")) } /// Get open interest. /// /// For example: /// - pub fn fetch_open_interest() -> Result { gen_api!("/futures") } } ================================================ FILE: crypto-rest-client/src/exchanges/gate/gate_future.rs ================================================ use super::super::utils::http_get; use crate::error::Result; use std::collections::BTreeMap; const BASE_URL: &str = "https://api.gateio.ws/api/v4"; /// The RESTful client for Gate Future markets. /// /// * RESTful API doc: /// * Trading at: pub struct GateFutureRestClient { _api_key: Option, _api_secret: Option, } impl GateFutureRestClient { pub fn new(api_key: Option, api_secret: Option) -> Self { GateFutureRestClient { _api_key: api_key, _api_secret: api_secret } } /// Get the latest Level2 snapshot of orderbook. /// /// Top 50 asks and bids are returned. /// /// For example: /// /// - /// - pub fn fetch_l2_snapshot(symbol: &str) -> Result { let without_date = &symbol[..(symbol.len() - 8)]; let settle = if without_date.ends_with("_USD_") { "btc" } else if without_date.ends_with("_USDT_") { "usdt" } else { panic!("Unknown symbol {symbol}"); }; gen_api!(format!("/delivery/{settle}/order_book?contract={symbol}&limit=50")) } } ================================================ FILE: crypto-rest-client/src/exchanges/gate/gate_spot.rs ================================================ use super::super::utils::http_get; use crate::error::Result; use std::collections::BTreeMap; const BASE_URL: &str = "https://api.gateio.ws/api/v4"; /// The RESTful client for Gate spot market. /// /// * RESTful API doc: /// * Trading at: /// * Rate Limits: /// * 300 read operations per IP per second pub struct GateSpotRestClient { _api_key: Option, _api_secret: Option, } impl GateSpotRestClient { pub fn new(api_key: Option, api_secret: Option) -> Self { GateSpotRestClient { _api_key: api_key, _api_secret: api_secret } } /// Get the latest Level2 snapshot of orderbook. /// /// Top 1000 asks and bids are returned. /// /// For example: , pub fn fetch_l2_snapshot(symbol: &str) -> Result { gen_api!(format!("/spot/order_book?currency_pair={symbol}&limit=1000")) } } ================================================ FILE: crypto-rest-client/src/exchanges/gate/gate_swap.rs ================================================ use super::super::utils::http_get; use crate::error::Result; use std::collections::BTreeMap; const BASE_URL: &str = "https://api.gateio.ws/api/v4"; /// The RESTful client for Gate Swap markets. /// /// * RESTful API doc: /// * Trading at: /// * Rate Limits: /// * 300 read operations per IP per second pub struct GateSwapRestClient { _api_key: Option, _api_secret: Option, } impl GateSwapRestClient { pub fn new(api_key: Option, api_secret: Option) -> Self { GateSwapRestClient { _api_key: api_key, _api_secret: api_secret } } /// Get the latest Level2 snapshot of orderbook. /// /// Top 200 asks and bids are returned. /// /// For example: /// /// - /// - pub fn fetch_l2_snapshot(symbol: &str) -> Result { let settle = if symbol.ends_with("_USD") { "btc" } else if symbol.ends_with("_USDT") { "usdt" } else { panic!("Unknown symbol {symbol}"); }; gen_api!(format!("/futures/{settle}/order_book?contract={symbol}&limit=200")) } /// Get open interest. /// /// For example: /// - /// - pub fn fetch_open_interest(symbol: &str) -> Result { let settle = if symbol.ends_with("_USD") { "btc" } else if symbol.ends_with("_USDT") { "usdt" } else { panic!("Unknown symbol {symbol}"); }; gen_api!(format!("/futures/{settle}/contract_stats?contract={symbol}&interval=5m")) } } ================================================ FILE: crypto-rest-client/src/exchanges/gate/mod.rs ================================================ mod gate_future; mod gate_spot; mod gate_swap; pub use gate_future::GateFutureRestClient; pub use gate_spot::GateSpotRestClient; pub use gate_swap::GateSwapRestClient; use crate::error::Result; use crypto_market_type::MarketType; pub(crate) fn fetch_l2_snapshot(market_type: MarketType, symbol: &str) -> Result { let func = match market_type { MarketType::Spot => gate_spot::GateSpotRestClient::fetch_l2_snapshot, MarketType::InverseSwap | MarketType::LinearSwap => { gate_swap::GateSwapRestClient::fetch_l2_snapshot } MarketType::InverseFuture | MarketType::LinearFuture => { gate_future::GateFutureRestClient::fetch_l2_snapshot } _ => panic!("Gate unknown market_type: {market_type}"), }; func(symbol) } pub(crate) fn fetch_open_interest(market_type: MarketType, symbol: &str) -> Result { let func = match market_type { MarketType::InverseSwap | MarketType::LinearSwap => { gate_swap::GateSwapRestClient::fetch_open_interest } _ => panic!("Gate {market_type} does NOT have open interest data"), }; func(symbol) } ================================================ FILE: crypto-rest-client/src/exchanges/huobi/huobi_future.rs ================================================ use super::super::utils::http_get; use crate::error::Result; use std::collections::BTreeMap; const BASE_URL: &str = "https://api.hbdm.com"; /// Huobi Future market. /// /// * REST API doc: /// * Trading at: /// * Rate Limits: /// * For restful interfaces:all products(futures, coin margined swap, usdt /// margined swap ) 800 times/second for one IP at most pub struct HuobiFutureRestClient { _api_key: Option, _api_secret: Option, } impl_contract!(HuobiFutureRestClient); impl HuobiFutureRestClient { /// Get the latest Level2 orderbook snapshot. /// /// Top 150 bids and asks (aggregated) are returned. /// /// For example: pub fn fetch_l2_snapshot(symbol: &str) -> Result { gen_api!(format!("/market/depth?symbol={symbol}&type=step0")) } /// Get open interest. /// /// For example: pub fn fetch_open_interest(symbol: Option<&str>) -> Result { if let Some(symbol) = symbol { gen_api!(format!("/api/v1/contract_open_interest?contract_code={symbol}")) } else { gen_api!("/api/v1/contract_open_interest") } } } ================================================ FILE: crypto-rest-client/src/exchanges/huobi/huobi_inverse_swap.rs ================================================ use super::super::utils::http_get; use crate::error::Result; use std::collections::BTreeMap; const BASE_URL: &str = "https://api.hbdm.com"; /// Huobi Inverse Swap market. /// /// Inverse Swap market uses coins like BTC as collateral. /// /// * REST API doc: /// * Trading at: /// * Rate Limits: /// * For restful interfaces:all products(futures, coin margined swap, usdt /// margined swap) 800 times/second for one IP at most pub struct HuobiInverseSwapRestClient { _api_key: Option, _api_secret: Option, } impl_contract!(HuobiInverseSwapRestClient); impl HuobiInverseSwapRestClient { /// Get the latest Level2 orderbook snapshot. /// /// Top 150 bids and asks (aggregated) are returned. /// /// For example: pub fn fetch_l2_snapshot(symbol: &str) -> Result { gen_api!(format!("/swap-ex/market/depth?contract_code={symbol}&type=step0")) } /// Get open interest. /// /// For example: pub fn fetch_open_interest(symbol: Option<&str>) -> Result { if let Some(symbol) = symbol { gen_api!(format!("/swap-api/v1/swap_open_interest?contract_code={symbol}")) } else { gen_api!("/swap-api/v1/swap_open_interest") } } } ================================================ FILE: crypto-rest-client/src/exchanges/huobi/huobi_linear_swap.rs ================================================ use super::super::utils::http_get; use crate::error::Result; use std::collections::BTreeMap; const BASE_URL: &str = "https://api.hbdm.com"; /// Huobi Linear Swap market. /// /// Linear Swap market uses USDT as collateral. /// /// * REST API doc: /// * Trading at: /// * Rate Limits: /// * For restful interfaces, products, (future, coin margined swap, usdt /// margined swap)800 times/second for one IP at most pub struct HuobiLinearSwapRestClient { _api_key: Option, _api_secret: Option, } impl_contract!(HuobiLinearSwapRestClient); impl HuobiLinearSwapRestClient { /// Get the latest Level2 orderbook snapshot. /// /// Top 150 bids and asks (aggregated) are returned. /// /// For example: pub fn fetch_l2_snapshot(symbol: &str) -> Result { gen_api!(format!("/linear-swap-ex/market/depth?contract_code={symbol}&type=step0")) } /// Get open interest. /// /// For example: pub fn fetch_open_interest(symbol: Option<&str>) -> Result { if let Some(symbol) = symbol { gen_api!(format!("/linear-swap-api/v1/swap_open_interest?contract_code={symbol}")) } else { gen_api!("/linear-swap-api/v1/swap_open_interest") } } } ================================================ FILE: crypto-rest-client/src/exchanges/huobi/huobi_option.rs ================================================ use super::super::utils::http_get; use crate::error::Result; use std::collections::BTreeMap; const BASE_URL: &str = "https://api.hbdm.com/option-ex"; /// Huobi Option market. /// /// /// * REST API doc: /// * Trading at: /// * Rate Limits: /// * For restful interfaces:all products(futures, coin margined swap, usdt /// margined swap and option) 800 times/second for one IP at most pub struct HuobiOptionRestClient { _api_key: Option, _api_secret: Option, } impl_contract!(HuobiOptionRestClient); impl HuobiOptionRestClient { /// Get the latest Level2 orderbook snapshot. /// /// Top 150 bids and asks (aggregated) are returned. /// /// For example: pub fn fetch_l2_snapshot(symbol: &str) -> Result { gen_api!(format!("/market/depth?contract_code={symbol}&type=step0")) } } ================================================ FILE: crypto-rest-client/src/exchanges/huobi/huobi_spot.rs ================================================ use super::super::utils::http_get; use crate::error::Result; use std::collections::BTreeMap; const BASE_URL: &str = "https://api.huobi.pro"; /// Huobi Spot market. /// /// * REST API doc: /// * Trading at: /// * Rate Limits: /// * If API Key is empty in request, then each IP is limited to 10 times per /// second pub struct HuobiSpotRestClient { _api_key: Option, _api_secret: Option, } impl_contract!(HuobiSpotRestClient); impl HuobiSpotRestClient { /// Get the latest Level2 orderbook snapshot. /// /// Top 150 bids and asks (aggregated) are returned. /// /// For example: pub fn fetch_l2_snapshot(symbol: &str) -> Result { gen_api!(format!("/market/depth?symbol={symbol}&type=step0")) } } ================================================ FILE: crypto-rest-client/src/exchanges/huobi/mod.rs ================================================ #[macro_use] mod utils; pub(crate) mod huobi_future; pub(crate) mod huobi_inverse_swap; pub(crate) mod huobi_linear_swap; pub(crate) mod huobi_option; pub(crate) mod huobi_spot; use crate::error::{Error, Result}; use crypto_market_type::MarketType; pub(crate) fn fetch_l2_snapshot(market_type: MarketType, symbol: &str) -> Result { let func = match market_type { MarketType::Spot => huobi_spot::HuobiSpotRestClient::fetch_l2_snapshot, MarketType::InverseFuture => huobi_future::HuobiFutureRestClient::fetch_l2_snapshot, MarketType::LinearSwap => huobi_linear_swap::HuobiLinearSwapRestClient::fetch_l2_snapshot, MarketType::InverseSwap => { huobi_inverse_swap::HuobiInverseSwapRestClient::fetch_l2_snapshot } MarketType::EuropeanOption => huobi_option::HuobiOptionRestClient::fetch_l2_snapshot, _ => panic!("Binance unknown market_type: {market_type}"), }; // if msg is {"status": "maintain"}, convert it to an error match func(symbol) { Ok(msg) => { if msg == r#"{"status": "maintain"}"# { Err(Error(msg)) } else { Ok(msg) } } Err(err) => Err(err), } } pub(crate) fn fetch_open_interest(market_type: MarketType, symbol: Option<&str>) -> Result { let func = match market_type { MarketType::InverseFuture => huobi_future::HuobiFutureRestClient::fetch_open_interest, MarketType::LinearSwap => huobi_linear_swap::HuobiLinearSwapRestClient::fetch_open_interest, MarketType::InverseSwap => { huobi_inverse_swap::HuobiInverseSwapRestClient::fetch_open_interest } _ => panic!("Huobi {market_type} does not have open interest"), }; // if msg is {"status": "maintain"}, convert it to an error match func(symbol) { Ok(msg) => { if msg == r#"{"status": "maintain"}"# { Err(Error(msg)) } else { Ok(msg) } } Err(err) => Err(err), } } ================================================ FILE: crypto-rest-client/src/exchanges/huobi/utils.rs ================================================ macro_rules! impl_contract { ($struct_name:ident) => { impl $struct_name { pub fn new(api_key: Option, api_secret: Option) -> Self { Self { _api_key: api_key, _api_secret: api_secret } } /// Get the most recent trades. /// /// Equivalent to `/market/history/trade` with `size=2000` /// /// For example: pub fn fetch_trades(symbol: &str) -> Result { gen_api!(format!("/market/history/trade?symbol={}&size=2000", symbol)) } } }; } ================================================ FILE: crypto-rest-client/src/exchanges/kraken/kraken_futures.rs ================================================ use super::super::utils::http_get; use crate::error::Result; use std::collections::BTreeMap; // see https://support.kraken.com/hc/en-us/articles/360022839491-API-URLs const BASE_URL: &str = "https://futures.kraken.com/derivatives/api/v3"; /// The WebSocket client for Kraken Futures. /// /// * REST API doc: /// * Trading at: /// * Rate Limits: /// * 500 every 10 seconds pub struct KrakenFuturesRestClient { _api_key: Option, _api_secret: Option, } impl KrakenFuturesRestClient { pub fn new(api_key: Option, api_secret: Option) -> Self { KrakenFuturesRestClient { _api_key: api_key, _api_secret: api_secret } } /// Get most recent trades. /// /// If `lastTime` is provided, return trade data from the specified /// lastTime. /// /// For example: #[allow(non_snake_case)] pub fn fetch_trades(symbol: &str, lastTime: Option) -> Result { gen_api!(format!("/history?symbol={symbol}"), lastTime) } /// Get a Level2 snapshot of orderbook. /// /// For example: pub fn fetch_l2_snapshot(symbol: &str) -> Result { gen_api!(format!("/orderbook?symbol={symbol}")) } } ================================================ FILE: crypto-rest-client/src/exchanges/kraken/kraken_spot.rs ================================================ use super::super::utils::http_get; use crate::error::Result; use std::collections::BTreeMap; const BASE_URL: &str = "https://api.kraken.com"; /// The WebSocket client for Kraken. /// /// Kraken has only Spot market. /// /// * REST API doc: /// * Trading at: /// * Rate Limits: /// * 15 requests per 45 seconds pub struct KrakenSpotRestClient { _api_key: Option, _api_secret: Option, } impl KrakenSpotRestClient { pub fn new(api_key: Option, api_secret: Option) -> Self { KrakenSpotRestClient { _api_key: api_key, _api_secret: api_secret } } /// Get most recent trades. /// /// If `since` is provided, return trade data since given id (exclusive). /// /// For example: #[allow(non_snake_case)] pub fn fetch_trades(symbol: &str, since: Option) -> Result { if symbol.contains('/') { // websocket and RESTful API have different symbol format // XBT/USD -> XBTUSD let stripped = symbol.replace('/', ""); gen_api!(format!("/0/public/Trades?pair={}", &stripped), since) } else { gen_api!(format!("/0/public/Trades?pair={symbol}"), since) } } /// Get a Level2 snapshot of orderbook. /// /// Top 500 bids and asks are returned. /// /// For example: pub fn fetch_l2_snapshot(symbol: &str) -> Result { if symbol.contains('/') { // websocket and RESTful API have different symbol format // XBT/USD -> XBTUSD let stripped = symbol.replace('/', ""); gen_api!(format!("/0/public/Depth?pair={stripped}&count=500")) } else { gen_api!(format!("/0/public/Depth?pair={symbol}&count=500")) } } } ================================================ FILE: crypto-rest-client/src/exchanges/kraken/mod.rs ================================================ pub(crate) mod kraken_futures; pub(crate) mod kraken_spot; use crate::error::Result; use crypto_market_type::MarketType; pub(crate) fn fetch_l2_snapshot(market_type: MarketType, symbol: &str) -> Result { let func = match market_type { MarketType::Spot => kraken_spot::KrakenSpotRestClient::fetch_l2_snapshot, MarketType::InverseFuture | MarketType::InverseSwap => { kraken_futures::KrakenFuturesRestClient::fetch_l2_snapshot } _ => panic!("Kraken unknown market_type: {market_type}"), }; func(symbol) } ================================================ FILE: crypto-rest-client/src/exchanges/kucoin/kucoin_spot.rs ================================================ use super::super::utils::http_get; use crate::error::Result; use std::collections::BTreeMap; const BASE_URL: &str = "https://api.kucoin.com"; /// The RESTful client for KuCoin spot market. /// /// * RESTful API doc: /// * Trading at: /// * Rate Limits: pub struct KuCoinSpotRestClient { _api_key: Option, _api_secret: Option, } impl KuCoinSpotRestClient { pub fn new(api_key: Option, api_secret: Option) -> Self { KuCoinSpotRestClient { _api_key: api_key, _api_secret: api_secret } } /// Get the latest Level2 snapshot of orderbook. /// /// For example: , pub fn fetch_l2_snapshot(symbol: &str) -> Result { let api = if std::env::var("KC-API-KEY").is_ok() { // the request rate limit is 30 times/3s "/api/v3/market/orderbook/level2" } else { "/api/v1/market/orderbook/level2_100" }; gen_api!(format!("{api}?symbol={symbol}")) } /// Get the latest Level3 snapshot of orderbook. /// /// All bids and asks are returned. /// /// For example: , pub fn fetch_l3_snapshot(symbol: &str) -> Result { gen_api!(format!("/api/v2/market/orderbook/level3?symbol={symbol}")) } } ================================================ FILE: crypto-rest-client/src/exchanges/kucoin/kucoin_swap.rs ================================================ use super::super::utils::http_get; use crate::error::Result; use std::collections::BTreeMap; const BASE_URL: &str = "https://api-futures.kucoin.com"; /// The RESTful client for KuCoin Future and Swap markets. /// /// * RESTful API doc: /// * Trading at: /// * Rate Limits: pub struct KuCoinSwapRestClient { _api_key: Option, _api_secret: Option, } impl KuCoinSwapRestClient { pub fn new(api_key: Option, api_secret: Option) -> Self { KuCoinSwapRestClient { _api_key: api_key, _api_secret: api_secret } } /// Get the latest Level2 snapshot of orderbook. /// /// All bids and asks are returned. /// /// For example: , pub fn fetch_l2_snapshot(symbol: &str) -> Result { // the request rate limit is 30 times/3s gen_api!(format!("/api/v1/level2/snapshot?symbol={symbol}")) } /// Get the latest Level3 snapshot of orderbook. /// /// All bids and asks are returned. /// /// For example: , pub fn fetch_l3_snapshot(symbol: &str) -> Result { gen_api!(format!("/api/v2/level3/snapshot?symbol={symbol}")) } /// Get open interest. /// /// For example: /// - pub fn fetch_open_interest() -> Result { gen_api!("/api/v1/contracts/active") } } ================================================ FILE: crypto-rest-client/src/exchanges/kucoin/mod.rs ================================================ mod kucoin_spot; mod kucoin_swap; pub use kucoin_spot::KuCoinSpotRestClient; pub use kucoin_swap::KuCoinSwapRestClient; use crate::error::Result; use crypto_market_type::MarketType; pub(crate) fn fetch_l2_snapshot(market_type: MarketType, symbol: &str) -> Result { let func = match market_type { MarketType::Spot => kucoin_spot::KuCoinSpotRestClient::fetch_l2_snapshot, MarketType::InverseSwap | MarketType::LinearSwap | MarketType::InverseFuture => { kucoin_swap::KuCoinSwapRestClient::fetch_l2_snapshot } _ => panic!("Bitget unknown market_type: {market_type}"), }; func(symbol) } pub(crate) fn fetch_l3_snapshot(market_type: MarketType, symbol: &str) -> Result { let func = match market_type { MarketType::Spot => kucoin_spot::KuCoinSpotRestClient::fetch_l3_snapshot, MarketType::InverseSwap | MarketType::LinearSwap | MarketType::InverseFuture => { kucoin_swap::KuCoinSwapRestClient::fetch_l3_snapshot } _ => panic!("Bitget unknown market_type: {market_type}"), }; func(symbol) } pub(crate) fn fetch_open_interest(market_type: MarketType) -> Result { match market_type { MarketType::InverseSwap | MarketType::LinearSwap | MarketType::Unknown => { kucoin_swap::KuCoinSwapRestClient::fetch_open_interest() } _ => panic!("kucoin {market_type} does not have open interest"), } } ================================================ FILE: crypto-rest-client/src/exchanges/mexc/mexc_spot.rs ================================================ use super::super::utils::http_get; use crate::error::Result; use std::collections::BTreeMap; const BASE_URL: &str = "https://www.mexc.com"; /// MEXC Spot market. /// /// * REST API doc: /// * Trading at: /// * Rate Limits: /// * The default rate limiting rule for an endpoint is 20 times per second. pub struct MexcSpotRestClient { _access_key: String, _secret_key: Option, } impl MexcSpotRestClient { pub fn new(access_key: String, secret_key: Option) -> Self { MexcSpotRestClient { _access_key: access_key, _secret_key: secret_key } } /// Get latest trades. /// /// 1000 trades are returned. /// /// For example: #[allow(non_snake_case)] pub fn fetch_trades(symbol: &str) -> Result { gen_api!(format!("/open/api/v2/market/deals?symbol={symbol}&limit=1000")) } /// Get latest Level2 snapshot of orderbook. /// /// Top 2000 bids and asks will be returned. /// /// For example: pub fn fetch_l2_snapshot(symbol: &str) -> Result { gen_api!(format!("/open/api/v2/market/depth?symbol={symbol}&depth=2000")) } } ================================================ FILE: crypto-rest-client/src/exchanges/mexc/mexc_swap.rs ================================================ use super::super::utils::http_get; use crate::error::Result; use std::collections::BTreeMap; const BASE_URL: &str = "https://contract.mexc.com"; /// MEXC Swap market. /// /// * REST API doc: /// * Trading at: pub struct MexcSwapRestClient { _api_key: Option, _api_secret: Option, } impl MexcSwapRestClient { pub fn new(api_key: Option, api_secret: Option) -> Self { MexcSwapRestClient { _api_key: api_key, _api_secret: api_secret } } /// Get most recent trades. /// /// For example: pub fn fetch_trades(symbol: &str) -> Result { gen_api!(format!("/api/v1/contract/deals/{symbol}")) } /// Get the latest Level2 snapshot of orderbook. /// /// Top 2000 bids and asks will be returned. /// /// For example: /// /// Rate limit: 20 times /2 seconds pub fn fetch_l2_snapshot(symbol: &str) -> Result { gen_api!(format!("/api/v1/contract/depth/{symbol}?limit=2000")) } } ================================================ FILE: crypto-rest-client/src/exchanges/mexc/mod.rs ================================================ pub(crate) mod mexc_spot; pub(crate) mod mexc_swap; use crate::error::Result; use crypto_market_type::MarketType; pub(crate) fn fetch_l2_snapshot(market_type: MarketType, symbol: &str) -> Result { let func = match market_type { MarketType::Spot => mexc_spot::MexcSpotRestClient::fetch_l2_snapshot, MarketType::InverseSwap | MarketType::LinearSwap => { mexc_swap::MexcSwapRestClient::fetch_l2_snapshot } _ => panic!("MEXC unknown market_type: {market_type}"), }; func(symbol) } ================================================ FILE: crypto-rest-client/src/exchanges/mod.rs ================================================ #[macro_use] mod utils; pub(super) mod binance; pub(super) mod bitfinex; pub(super) mod bitget; pub(super) mod bithumb; pub(super) mod bitmex; pub(super) mod bitstamp; pub(super) mod bitz; pub(super) mod bybit; pub(super) mod coinbase_pro; pub(super) mod deribit; pub(super) mod dydx; pub(super) mod ftx; pub(super) mod gate; pub(super) mod huobi; pub(super) mod kraken; pub(super) mod kucoin; pub(super) mod mexc; pub(super) mod okx; pub(super) mod zb; pub(super) mod zbg; ================================================ FILE: crypto-rest-client/src/exchanges/okx.rs ================================================ use super::utils::http_get; use crate::error::Result; use crypto_market_type::MarketType; use serde_json::Value; use std::collections::{BTreeMap, HashMap}; const BASE_URL: &str = "https://www.okx.com"; /// The REST client for OKEx. /// /// OKEx has Spot, Future, Swap and Option markets. /// /// * API doc: /// * Trading at: /// * Spot /// * Future /// * Swap /// * Option pub struct OkxRestClient { _api_key: Option, _api_secret: Option, } impl OkxRestClient { pub fn new(api_key: Option, api_secret: Option) -> Self { OkxRestClient { _api_key: api_key, _api_secret: api_secret } } /// Get most recent trades. /// /// 500 trades are returned. /// /// For example: pub fn fetch_trades(symbol: &str) -> Result { gen_api!(format!("/api/v5/market/trades?instId={symbol}&limit=500")) } /// Get the latest Level2 snapshot of orderbook. /// /// Top 400 bids and asks are returned. /// /// For example: /// * , /// * /// /// Rate limit: 20 requests per 2 seconds pub fn fetch_l2_snapshot(symbol: &str) -> Result { gen_api!(format!("/api/v5/market/books?instId={symbol}&sz=400",)) } /// Get option underlying. pub fn fetch_option_underlying() -> Result> { let txt = http_get( "https://www.okx.com/api/v5/public/underlying?instType=OPTION", &BTreeMap::new(), )?; let json_obj = serde_json::from_str::>(&txt).unwrap(); let data = json_obj.get("data").unwrap().as_array().unwrap()[0].as_array().unwrap(); let underlying_indexes = data.iter().map(|x| x.as_str().unwrap().to_string()).collect::>(); Ok(underlying_indexes) } /// Get open interest. /// /// inst_type: SWAP, FUTURES, OPTION /// /// For example: /// - /// - pub fn fetch_open_interest(market_type: MarketType, symbol: Option<&str>) -> Result { let inst_type = match market_type { MarketType::LinearFuture => "FUTURES", MarketType::InverseFuture => "FUTURES", MarketType::LinearSwap => "SWAP", MarketType::InverseSwap => "SWAP", MarketType::EuropeanOption => "OPTION", _ => panic!("okx {market_type} doesn't have open interest"), }; if let Some(inst_id) = symbol { gen_api!(format!("/api/v5/public/open-interest?instType={inst_type}&instId={inst_id}",)) } else { gen_api!(format!("/api/v5/public/open-interest?instType={inst_type}")) } } } ================================================ FILE: crypto-rest-client/src/exchanges/utils.rs ================================================ use reqwest::{blocking::Response, header}; use crate::error::{Error, Result}; use std::collections::BTreeMap; // Returns the raw response directly. pub(super) fn http_get_raw(url: &str, params: &BTreeMap) -> Result { let mut full_url = url.to_string(); let mut first = true; for (k, v) in params.iter() { if first { full_url.push_str(format!("?{k}={v}").as_str()); first = false; } else { full_url.push_str(format!("&{k}={v}").as_str()); } } // println!("{}", full_url); let mut headers = header::HeaderMap::new(); headers.insert(header::CONTENT_TYPE, header::HeaderValue::from_static("application/json")); let client = reqwest::blocking::Client::builder() .default_headers(headers) .user_agent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36") .gzip(true) .build()?; let response = client.get(full_url.as_str()).send()?; Ok(response) } // Returns the text in response. pub(super) fn http_get(url: &str, params: &BTreeMap) -> Result { match http_get_raw(url, params) { Ok(response) => match response.error_for_status() { Ok(resp) => Ok(resp.text()?), Err(error) => Err(Error::from(error)), }, Err(err) => Err(err), } } macro_rules! gen_api { ( $path:expr$(, $param_name:ident )* ) => { { #[allow(unused_mut)] let mut params = BTreeMap::new(); $( if let Some(param_name) = $param_name { params.insert(stringify!($param_name).to_string(), param_name.to_string()); } )* let url = if $path.starts_with("http") { $path.to_string() } else { format!("{}{}",BASE_URL, $path) }; http_get(&url, ¶ms) } } } #[cfg(test)] mod tests { use std::collections::BTreeMap; use serde_json::Value; // System proxies are enabled by default, see #[test] #[ignore] fn use_system_socks_proxy() { std::env::set_var("https_proxy", "socks5://127.0.0.1:9050"); let text = super::http_get("https://check.torproject.org/api/ip", &BTreeMap::new()).unwrap(); let obj = serde_json::from_str::>(&text).unwrap(); assert!(obj.get("IsTor").unwrap().as_bool().unwrap()); } #[test] #[ignore] fn use_system_https_proxy() { std::env::set_var("https_proxy", "http://127.0.0.1:8118"); let text = super::http_get("https://check.torproject.org/api/ip", &BTreeMap::new()).unwrap(); let obj = serde_json::from_str::>(&text).unwrap(); assert!(obj.get("IsTor").unwrap().as_bool().unwrap()); } } ================================================ FILE: crypto-rest-client/src/exchanges/zb/mod.rs ================================================ mod zb_spot; mod zb_swap; pub use zb_spot::ZbSpotRestClient; pub use zb_swap::ZbSwapRestClient; use crate::error::Result; use crypto_market_type::MarketType; pub(crate) fn fetch_l2_snapshot(market_type: MarketType, symbol: &str) -> Result { let func = match market_type { MarketType::Spot => zb_spot::ZbSpotRestClient::fetch_l2_snapshot, MarketType::InverseSwap | MarketType::LinearSwap => { zb_swap::ZbSwapRestClient::fetch_l2_snapshot } _ => panic!("ZBG unknown market_type: {market_type}"), }; func(symbol) } ================================================ FILE: crypto-rest-client/src/exchanges/zb/zb_spot.rs ================================================ use super::super::utils::http_get; use crate::error::Result; use std::collections::BTreeMap; const BASE_URL: &str = "https://api.zb.com"; /// The RESTful client for ZB spot market. /// /// * RESTful API doc: /// * Trading at: pub struct ZbSpotRestClient { _api_key: Option, _api_secret: Option, } impl ZbSpotRestClient { pub fn new(api_key: Option, api_secret: Option) -> Self { ZbSpotRestClient { _api_key: api_key, _api_secret: api_secret } } /// Get the latest Level2 snapshot of orderbook. /// /// Top 50 bids and asks are returned. /// /// For example: , pub fn fetch_l2_snapshot(symbol: &str) -> Result { gen_api!(format!("/data/v1/depth?market={symbol}&size=50")) } } ================================================ FILE: crypto-rest-client/src/exchanges/zb/zb_swap.rs ================================================ use super::super::utils::http_get; use crate::error::Result; use std::collections::BTreeMap; const BASE_URL: &str = "https://fapi.zb.com"; /// The RESTful client for ZB swap markets. /// /// * RESTful API doc: /// * Trading at: pub struct ZbSwapRestClient { _api_key: Option, _api_secret: Option, } impl ZbSwapRestClient { pub fn new(api_key: Option, api_secret: Option) -> Self { ZbSwapRestClient { _api_key: api_key, _api_secret: api_secret } } /// Get the latest Level2 snapshot of orderbook. /// /// Top 200 bids and asks are returned. /// /// For example: /// * /// * pub fn fetch_l2_snapshot(symbol: &str) -> Result { if symbol.ends_with("_QC") { gen_api!(format!("/qc/api/public/v1/depth?symbol={symbol}&size=200")) } else { gen_api!(format!("/api/public/v1/depth?symbol={symbol}&size=200")) } } } ================================================ FILE: crypto-rest-client/src/exchanges/zbg/mod.rs ================================================ mod zbg_spot; mod zbg_swap; pub use zbg_spot::ZbgSpotRestClient; pub use zbg_swap::ZbgSwapRestClient; use crate::error::Result; use crypto_market_type::MarketType; pub(crate) fn fetch_l2_snapshot(market_type: MarketType, symbol: &str) -> Result { let func = match market_type { MarketType::Spot => zbg_spot::ZbgSpotRestClient::fetch_l2_snapshot, MarketType::InverseSwap | MarketType::LinearSwap => { zbg_swap::ZbgSwapRestClient::fetch_l2_snapshot } _ => panic!("ZBG unknown market_type: {market_type}"), }; func(symbol) } pub(crate) fn fetch_open_interest(market_type: MarketType, symbol: &str) -> Result { let func = match market_type { MarketType::InverseSwap | MarketType::LinearSwap => { zbg_swap::ZbgSwapRestClient::fetch_open_interest } _ => panic!("ZBG {market_type} does NOT have open interest data"), }; func(symbol) } ================================================ FILE: crypto-rest-client/src/exchanges/zbg/zbg_spot.rs ================================================ use super::super::utils::http_get; use crate::error::Result; use std::collections::BTreeMap; const BASE_URL: &str = "https://kline.zbg.com"; /// The RESTful client for ZBG spot market. /// /// * RESTful API doc: /// * Trading at: pub struct ZbgSpotRestClient { _api_key: Option, _api_secret: Option, } impl ZbgSpotRestClient { pub fn new(api_key: Option, api_secret: Option) -> Self { ZbgSpotRestClient { _api_key: api_key, _api_secret: api_secret } } /// Get the latest Level2 snapshot of orderbook. /// /// Top 200 bids and asks are returned. /// /// For example: , pub fn fetch_l2_snapshot(symbol: &str) -> Result { gen_api!(format!("/api/data/v1/entrusts?marketName={symbol}&dataSize=200")) } } ================================================ FILE: crypto-rest-client/src/exchanges/zbg/zbg_swap.rs ================================================ use super::super::utils::http_get; use crate::error::Result; use std::collections::BTreeMap; const BASE_URL: &str = "https://www.zbg.com"; /// The RESTful client for ZBG swap markets. /// /// * RESTful API doc: /// * Trading at: pub struct ZbgSwapRestClient { _api_key: Option, _api_secret: Option, } impl ZbgSwapRestClient { pub fn new(api_key: Option, api_secret: Option) -> Self { ZbgSwapRestClient { _api_key: api_key, _api_secret: api_secret } } /// Get the latest Level2 snapshot of orderbook. /// /// Top 200 bids and asks are returned. /// /// For example: pub fn fetch_l2_snapshot(symbol: &str) -> Result { gen_api!(format!("/exchange/api/v1/future/market/depth?symbol={symbol}&size=1000")) } /// Get open interest. /// /// For example: /// /// - pub fn fetch_open_interest(symbol: &str) -> Result { gen_api!(format!("/exchange/api/v1/future/market/ticker?symbol={symbol}")) } } ================================================ FILE: crypto-rest-client/src/lib.rs ================================================ mod error; mod exchanges; pub use error::Error; pub use exchanges::{ binance::{ binance_inverse::BinanceInverseRestClient, binance_linear::BinanceLinearRestClient, binance_option::BinanceOptionRestClient, binance_spot::BinanceSpotRestClient, }, bitfinex::BitfinexRestClient, bitget::*, bithumb::*, bitmex::BitmexRestClient, bitstamp::BitstampRestClient, bitz::*, bybit::BybitRestClient, coinbase_pro::CoinbaseProRestClient, deribit::DeribitRestClient, dydx::dydx_swap::DydxSwapRestClient, ftx::FtxRestClient, gate::*, huobi::{ huobi_future::HuobiFutureRestClient, huobi_inverse_swap::HuobiInverseSwapRestClient, huobi_linear_swap::HuobiLinearSwapRestClient, huobi_option::HuobiOptionRestClient, huobi_spot::HuobiSpotRestClient, }, kraken::{kraken_futures::KrakenFuturesRestClient, kraken_spot::KrakenSpotRestClient}, kucoin::*, mexc::{mexc_spot::MexcSpotRestClient, mexc_swap::MexcSwapRestClient}, okx::OkxRestClient, zb::*, zbg::*, }; use crypto_market_type::MarketType; use error::Result; use log::*; use std::time::{Duration, SystemTime}; fn fetch_l2_snapshot_internal( exchange: &str, market_type: MarketType, symbol: &str, ) -> Result { let ret = match exchange { "binance" => exchanges::binance::fetch_l2_snapshot(market_type, symbol), "bitfinex" => exchanges::bitfinex::BitfinexRestClient::fetch_l2_snapshot(symbol), "bitget" => exchanges::bitget::fetch_l2_snapshot(market_type, symbol), "bithumb" => exchanges::bithumb::BithumbRestClient::fetch_l2_snapshot(symbol), "bitmex" => exchanges::bitmex::BitmexRestClient::fetch_l2_snapshot(symbol), "bitstamp" => exchanges::bitstamp::BitstampRestClient::fetch_l2_snapshot(symbol), "bitz" => exchanges::bitz::fetch_l2_snapshot(market_type, symbol), "bybit" => exchanges::bybit::BybitRestClient::fetch_l2_snapshot(symbol), "coinbase_pro" => exchanges::coinbase_pro::CoinbaseProRestClient::fetch_l2_snapshot(symbol), "deribit" => exchanges::deribit::DeribitRestClient::fetch_l2_snapshot(symbol), "dydx" => exchanges::dydx::fetch_l2_snapshot(market_type, symbol), "ftx" => exchanges::ftx::FtxRestClient::fetch_l2_snapshot(symbol), "gate" => exchanges::gate::fetch_l2_snapshot(market_type, symbol), "huobi" => exchanges::huobi::fetch_l2_snapshot(market_type, symbol), "kraken" => exchanges::kraken::fetch_l2_snapshot(market_type, symbol), "kucoin" => exchanges::kucoin::fetch_l2_snapshot(market_type, symbol), "mexc" => exchanges::mexc::fetch_l2_snapshot(market_type, symbol), "okx" => exchanges::okx::OkxRestClient::fetch_l2_snapshot(symbol), "zb" => exchanges::zb::fetch_l2_snapshot(market_type, symbol), "zbg" => exchanges::zbg::fetch_l2_snapshot(market_type, symbol), _ => panic!("Unknown exchange {exchange}"), }; match ret { Ok(s) => Ok(s.trim().to_string()), Err(_) => ret, } } pub fn fetch_l3_snapshot_internal( exchange: &str, market_type: MarketType, symbol: &str, ) -> Result { let ret = match exchange { "bitfinex" => exchanges::bitfinex::BitfinexRestClient::fetch_l3_snapshot(symbol), "bitstamp" => exchanges::bitstamp::BitstampRestClient::fetch_l3_snapshot(symbol), "coinbase_pro" => exchanges::coinbase_pro::CoinbaseProRestClient::fetch_l3_snapshot(symbol), "kucoin" => exchanges::kucoin::fetch_l3_snapshot(market_type, symbol), _ => panic!("{exchange} {market_type} does NOT provide level3 orderbook data"), }; match ret { Ok(s) => Ok(s.trim().to_string()), Err(_) => ret, } } /// Fetch open interest. /// /// `symbol` None means fetch all symbols. pub fn fetch_open_interest( exchange: &str, market_type: MarketType, symbol: Option<&str>, ) -> Result { let ret = match exchange { "binance" => exchanges::binance::fetch_open_interest(market_type, symbol.unwrap()), "bitget" => exchanges::bitget::fetch_open_interest(market_type, symbol.unwrap()), "bybit" => exchanges::bybit::BybitRestClient::fetch_open_interest(symbol.unwrap()), "bitz" => exchanges::bitz::fetch_open_interest(market_type, symbol), "deribit" => exchanges::deribit::DeribitRestClient::fetch_open_interest(symbol), "dydx" => exchanges::dydx::fetch_open_interest(market_type), "ftx" => exchanges::ftx::FtxRestClient::fetch_open_interest(), "gate" => exchanges::gate::fetch_open_interest(market_type, symbol.unwrap()), "huobi" => exchanges::huobi::fetch_open_interest(market_type, symbol), "kucoin" => exchanges::kucoin::fetch_open_interest(market_type), "okx" => exchanges::okx::OkxRestClient::fetch_open_interest(market_type, symbol), "zbg" => exchanges::zbg::fetch_open_interest(market_type, symbol.unwrap()), _ => panic!("{exchange} does NOT have open interest RESTful API"), }; match ret { Ok(s) => Ok(s.trim().to_string()), Err(_) => ret, } } pub fn fetch_long_short_ratio( exchange: &str, market_type: MarketType, symbol: &str, ) -> Result { let ret = match exchange { "bybit" => exchanges::bybit::BybitRestClient::fetch_long_short_ratio(symbol), _ => panic!("{exchange} {market_type} does NOT provide level3 orderbook data"), }; match ret { Ok(s) => Ok(s.trim().to_string()), Err(_) => ret, } } /// Fetch level2 orderbook snapshot. /// /// `retry` None means no retry; Some(0) means retry unlimited times; Some(n) /// means retry n times. pub fn fetch_l2_snapshot( exchange: &str, market_type: MarketType, symbol: &str, retry: Option, ) -> Result { retriable(exchange, market_type, symbol, fetch_l2_snapshot_internal, retry) } /// Fetch level3 orderbook snapshot. /// /// `retry` None means no retry; Some(0) means retry unlimited times; Some(n) /// means retry n times. pub fn fetch_l3_snapshot( exchange: &str, market_type: MarketType, symbol: &str, retry: Option, ) -> Result { retriable(exchange, market_type, symbol, fetch_l3_snapshot_internal, retry) } // `retry` None means no retry; Some(0) means retry unlimited times; Some(n) // means retry n times. fn retriable( exchange: &str, market_type: MarketType, symbol: &str, crawl_func: fn(&str, MarketType, &str) -> Result, retry: Option, ) -> Result { let retry_count = { let count = retry.unwrap_or(1); if count == 0 { u64::MAX } else { count } }; if retry_count == 1 { return crawl_func(exchange, market_type, symbol); } let mut backoff_factor = 0; let cooldown_time = Duration::from_secs(2); for _ in 0..retry_count { let resp = crawl_func(exchange, market_type, symbol); match resp { Ok(msg) => return Ok(msg), Err(err) => { let current_timestamp = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_millis() as u64; warn!( "{} {} {} {} {}, error: {}, back off for {} milliseconds", current_timestamp, backoff_factor, exchange, market_type, symbol, err, (backoff_factor * cooldown_time).as_millis() ); std::thread::sleep(backoff_factor * cooldown_time); if err.0.contains("429") { backoff_factor += 1; } else { // Handle 403, 418, etc. backoff_factor *= 2; } } } } Err(Error(format!( "Failed {exchange} {market_type} {symbol} after retrying {retry_count} times" ))) } ================================================ FILE: crypto-rest-client/tests/binance_inverse.rs ================================================ #[cfg(test)] mod inverse_swap { use crypto_market_type::MarketType; use crypto_rest_client::{fetch_l2_snapshot, fetch_open_interest, BinanceInverseRestClient}; #[test] fn test_agg_trades() { let text = BinanceInverseRestClient::fetch_agg_trades("BTCUSD_PERP", None, None, None).unwrap(); assert!(text.starts_with("[{")); } #[test] fn test_l2_snapshot() { let text = fetch_l2_snapshot("binance", MarketType::InverseSwap, "BTCUSD_PERP", Some(3)).unwrap(); assert!(text.starts_with('{')); } #[test] fn test_open_interest() { let text = fetch_open_interest("binance", MarketType::InverseSwap, Some("BTCUSD_PERP")).unwrap(); assert!(text.starts_with('{')); } } #[cfg(test)] mod inverse_future { use crypto_market_type::MarketType; use crypto_rest_client::{fetch_l2_snapshot, fetch_open_interest, BinanceInverseRestClient}; #[test] fn test_agg_trades() { let text = BinanceInverseRestClient::fetch_agg_trades("BTCUSD_221230", None, None, None).unwrap(); assert!(text.starts_with("[{")); } #[test] fn test_l2_snapshot() { let text = fetch_l2_snapshot("binance", MarketType::InverseFuture, "BTCUSD_221230", Some(3)) .unwrap(); assert!(text.starts_with('{')); } #[test] fn test_open_interest() { let text = fetch_open_interest("binance", MarketType::InverseFuture, Some("BTCUSD_221230")) .unwrap(); assert!(text.starts_with('{')); } } ================================================ FILE: crypto-rest-client/tests/binance_linear.rs ================================================ #[cfg(test)] mod linear_swap { use crypto_market_type::MarketType; use crypto_rest_client::{fetch_l2_snapshot, fetch_open_interest, BinanceLinearRestClient}; #[test] fn test_agg_trades() { let text = BinanceLinearRestClient::fetch_agg_trades("BTCUSDT", None, None, None).unwrap(); assert!(text.starts_with("[{")); } #[test] fn test_l2_snapshot() { let text = fetch_l2_snapshot("binance", MarketType::LinearSwap, "BTCUSDT", Some(3)).unwrap(); assert!(text.starts_with('{')); } #[test] fn test_open_interest() { let text = fetch_open_interest("binance", MarketType::LinearSwap, Some("BTCUSDT")).unwrap(); assert!(text.starts_with('{')); } } #[cfg(test)] mod linear_future { use crypto_market_type::MarketType; use crypto_rest_client::{fetch_l2_snapshot, fetch_open_interest, BinanceLinearRestClient}; #[test] fn test_agg_trades() { let text = BinanceLinearRestClient::fetch_agg_trades("BTCUSDT_221230", None, None, None).unwrap(); assert!(text.starts_with("[{")); } #[test] fn test_l2_snapshot() { let text = fetch_l2_snapshot("binance", MarketType::LinearFuture, "BTCUSDT_221230", Some(3)) .unwrap(); assert!(text.starts_with('{')); } #[test] fn test_open_interest() { let text = fetch_open_interest("binance", MarketType::LinearFuture, Some("BTCUSDT_221230")) .unwrap(); assert!(text.starts_with('{')); } } ================================================ FILE: crypto-rest-client/tests/binance_option.rs ================================================ use crypto_market_type::MarketType; use crypto_rest_client::{fetch_l2_snapshot, BinanceOptionRestClient}; #[ignore] #[test] fn test_agg_trades() { let text = BinanceOptionRestClient::fetch_trades("BTC-220610-30000-C", None).unwrap(); assert!(text.starts_with('{')); } #[ignore] #[test] fn test_l2_snapshot() { let text = fetch_l2_snapshot("binance", MarketType::EuropeanOption, "BTC-220610-30000-C", Some(3)) .unwrap(); assert!(text.starts_with('{')); } ================================================ FILE: crypto-rest-client/tests/binance_spot.rs ================================================ use crypto_market_type::MarketType; use crypto_rest_client::{fetch_l2_snapshot, BinanceSpotRestClient}; #[test] fn test_agg_trades() { let text = BinanceSpotRestClient::fetch_agg_trades("BTCUSDT", None, None, None).unwrap(); assert!(text.starts_with("[{")); } #[test] fn test_l2_snapshot() { let text = fetch_l2_snapshot("binance", MarketType::Spot, "BTCUSDT", Some(3)).unwrap(); assert!(text.starts_with('{')); } ================================================ FILE: crypto-rest-client/tests/bitfinex.rs ================================================ use crypto_market_type::MarketType; use crypto_rest_client::{fetch_l2_snapshot, fetch_l3_snapshot, BitfinexRestClient}; #[test] fn test_trades() { let text = BitfinexRestClient::fetch_trades("tBTCUSD", None, None, None, None).unwrap(); assert!(text.starts_with("[[")); } #[test] fn test_l2_snapshot() { let text = fetch_l2_snapshot("bitfinex", MarketType::Spot, "tBTCUSD", Some(3)).unwrap(); assert!(text.starts_with("[[")); } #[test] fn test_l3_snapshot() { let text = fetch_l3_snapshot("bitfinex", MarketType::Spot, "tBTCUSD", Some(3)).unwrap(); assert!(text.starts_with("[[")); } ================================================ FILE: crypto-rest-client/tests/bitget_spot.rs ================================================ use crypto_market_type::MarketType; use crypto_rest_client::fetch_l2_snapshot; use serde_json::Value; use std::collections::HashMap; #[test] fn test_l2_snapshot() { let text = fetch_l2_snapshot("bitget", MarketType::Spot, "BTCUSDT_SPBL", Some(3)).unwrap(); let obj = serde_json::from_str::>(&text).unwrap(); assert_eq!(obj.get("msg").unwrap().as_str().unwrap(), "success"); let data = obj.get("data").unwrap().as_object().unwrap(); assert!(!data.get("asks").unwrap().as_array().unwrap().is_empty()); assert!(!data.get("bids").unwrap().as_array().unwrap().is_empty()); } ================================================ FILE: crypto-rest-client/tests/bitget_swap.rs ================================================ use crypto_market_type::MarketType; use crypto_rest_client::{fetch_l2_snapshot, fetch_open_interest}; use serde_json::Value; use std::collections::HashMap; use test_case::test_case; #[test_case(MarketType::InverseFuture, "BTCUSD_DMCBL_221230")] #[test_case(MarketType::InverseSwap, "BTCUSD_DMCBL")] #[test_case(MarketType::LinearSwap, "BTCUSDT_UMCBL")] fn test_l2_snapshot(market_type: MarketType, symbol: &str) { let text = fetch_l2_snapshot("bitget", market_type, symbol, Some(3)).unwrap(); let obj = serde_json::from_str::>(&text).unwrap(); let data = obj.get("data").unwrap().as_object().unwrap(); assert!(!data.get("asks").unwrap().as_array().unwrap().is_empty()); assert!(!data.get("bids").unwrap().as_array().unwrap().is_empty()); } #[test_case(MarketType::InverseFuture, "BTCUSD_DMCBL_221230")] #[test_case(MarketType::InverseSwap, "BTCUSD_DMCBL")] #[test_case(MarketType::LinearSwap, "BTCUSDT_UMCBL")] fn test_open_interest(market_type: MarketType, symbol: &str) { let text = fetch_open_interest("bitget", market_type, Some(symbol)).unwrap(); let obj = serde_json::from_str::>(&text).unwrap(); let data = obj.get("data").unwrap().as_object().unwrap(); assert!(data.contains_key("amount")); } ================================================ FILE: crypto-rest-client/tests/bithumb.rs ================================================ use std::collections::HashMap; use crypto_market_type::MarketType; use crypto_rest_client::{fetch_l2_snapshot, BithumbRestClient}; use serde_json::Value; #[test] fn test_trades() { let text = BithumbRestClient::fetch_trades("BTC-USDT").unwrap(); let obj = serde_json::from_str::>(&text).unwrap(); assert_eq!(obj.get("code").unwrap().as_str().unwrap(), "0"); let data = obj.get("data").unwrap().as_array().unwrap(); assert!(!data.is_empty()); } #[test] fn test_l2_snapshot() { let text = fetch_l2_snapshot("bithumb", MarketType::Spot, "BTC-USDT", Some(3)).unwrap(); let obj = serde_json::from_str::>(&text).unwrap(); assert_eq!(obj.get("code").unwrap().as_str().unwrap(), "0"); let data = obj.get("data").unwrap().as_object().unwrap(); let buy = data.get("b").unwrap().as_array().unwrap(); let sell = data.get("s").unwrap().as_array().unwrap(); assert!(!buy.is_empty()); assert!(!sell.is_empty()); } ================================================ FILE: crypto-rest-client/tests/bitmex.rs ================================================ use crypto_market_type::MarketType; use crypto_rest_client::{fetch_l2_snapshot, BitmexRestClient}; #[test] fn test_trades() { let text = BitmexRestClient::fetch_trades("XBTUSD", None).unwrap(); assert!(text.starts_with("[{")); } #[test] fn test_l2_snapshot() { let text = fetch_l2_snapshot("bitmex", MarketType::InverseSwap, "XBTUSD", Some(3)).unwrap(); assert!(text.starts_with("[{")); } ================================================ FILE: crypto-rest-client/tests/bitstamp.rs ================================================ use crypto_market_type::MarketType; use crypto_rest_client::{fetch_l2_snapshot, fetch_l3_snapshot, BitstampRestClient}; #[test] fn test_trades() { let text = BitstampRestClient::fetch_trades("btcusd", Some("minute".to_string())).unwrap(); assert!(text.starts_with("[{")); } #[test] fn test_l2_snapshot() { let text = fetch_l2_snapshot("bitstamp", MarketType::Spot, "btcusd", Some(3)).unwrap(); assert!(text.starts_with('{')); } #[test] fn test_l3_snapshot() { let text = fetch_l3_snapshot("bitstamp", MarketType::Spot, "btcusd", Some(3)).unwrap(); assert!(text.starts_with('{')); } ================================================ FILE: crypto-rest-client/tests/bitz_spot.rs ================================================ use std::collections::HashMap; use crypto_market_type::MarketType; use crypto_rest_client::fetch_l2_snapshot; use serde_json::Value; #[test] #[ignore = "bitz.com has shutdown since October 2021"] fn test_l2_snapshot() { let text = fetch_l2_snapshot("bitz", MarketType::Spot, "btc_usdt", Some(3)).unwrap(); let obj = serde_json::from_str::>(&text).unwrap(); assert_eq!(obj.get("status").unwrap().as_i64().unwrap(), 200); let data = obj.get("data").unwrap().as_object().unwrap(); assert!(!data.get("asks").unwrap().as_array().unwrap().is_empty()); assert!(!data.get("bids").unwrap().as_array().unwrap().is_empty()); } ================================================ FILE: crypto-rest-client/tests/bitz_swap.rs ================================================ use std::collections::HashMap; use crypto_market_type::MarketType; use crypto_rest_client::{fetch_l2_snapshot, fetch_open_interest}; use serde_json::Value; use test_case::test_case; #[test_case(MarketType::InverseSwap, "BTC_USD"; "inconclusive 1")] #[test_case(MarketType::LinearSwap, "BTC_USDT"; "inconclusive 2")] fn test_l2_snapshot(market_type: MarketType, symbol: &str) { let text = fetch_l2_snapshot("bitz", market_type, symbol, Some(3)).unwrap(); let obj = serde_json::from_str::>(&text).unwrap(); assert_eq!(obj.get("status").unwrap().as_i64().unwrap(), 200); let data = obj.get("data").unwrap().as_object().unwrap(); assert!(!data.get("asks").unwrap().as_array().unwrap().is_empty()); assert!(!data.get("bids").unwrap().as_array().unwrap().is_empty()); } #[test_case(MarketType::InverseSwap; "inconclusive 1")] #[test_case(MarketType::LinearSwap; "inconclusive 2")] fn test_open_interest(market_type: MarketType) { let text = fetch_open_interest("bitz", market_type, None).unwrap(); let obj = serde_json::from_str::>(&text).unwrap(); let arr = obj.get("data").unwrap().as_array().unwrap(); assert!(!arr.is_empty()); } ================================================ FILE: crypto-rest-client/tests/bybit.rs ================================================ use std::collections::HashMap; use crypto_market_type::MarketType; use crypto_rest_client::{fetch_l2_snapshot, fetch_long_short_ratio, fetch_open_interest}; use serde_json::Value; use test_case::test_case; #[test_case(MarketType::InverseFuture, "BTCUSDZ22")] #[test_case(MarketType::InverseSwap, "BTCUSD")] #[test_case(MarketType::LinearSwap, "BTCUSDT")] fn test_l2_snapshot(market_type: MarketType, symbol: &str) { let text = fetch_l2_snapshot("bybit", market_type, symbol, Some(3)).unwrap(); let obj = serde_json::from_str::>(&text).unwrap(); let result = obj.get("result").unwrap(); assert!(result.is_array()); assert_eq!(result.as_array().unwrap().len(), 50); } #[test_case(MarketType::InverseFuture, "BTCUSDZ22")] #[test_case(MarketType::InverseSwap, "BTCUSD")] #[test_case(MarketType::LinearSwap, "BTCUSDT")] fn test_open_interest(market_type: MarketType, symbol: &str) { let text = fetch_open_interest("bybit", market_type, Some(symbol)).unwrap(); let obj = serde_json::from_str::>(&text).unwrap(); let result = obj.get("result").unwrap().as_array().unwrap(); assert!(!result.is_empty()); } #[test_case(MarketType::InverseFuture, "BTCUSDZ22")] #[test_case(MarketType::InverseSwap, "BTCUSD")] #[test_case(MarketType::LinearSwap, "BTCUSDT")] fn test_long_short_ratio(market_type: MarketType, symbol: &str) { let text = fetch_long_short_ratio("bybit", market_type, symbol).unwrap(); let obj = serde_json::from_str::>(&text).unwrap(); let result = obj.get("result").unwrap().as_array().unwrap(); assert!(!result.is_empty()); } ================================================ FILE: crypto-rest-client/tests/coinbase_pro.rs ================================================ use crypto_market_type::MarketType; use crypto_rest_client::{fetch_l2_snapshot, fetch_l3_snapshot, CoinbaseProRestClient}; #[test] fn test_trades() { let text = CoinbaseProRestClient::fetch_trades("BTC-USD").unwrap(); assert!(text.starts_with("[{")); } #[test] fn test_l2_snapshot() { let text = fetch_l2_snapshot("coinbase_pro", MarketType::Spot, "BTC-USD", Some(3)).unwrap(); assert!(text.starts_with('{')); } #[test] fn test_l3_snapshot() { let text = fetch_l3_snapshot("coinbase_pro", MarketType::Spot, "BTC-USD", Some(3)).unwrap(); assert!(text.starts_with('{')); } ================================================ FILE: crypto-rest-client/tests/deribit.rs ================================================ use crypto_market_type::MarketType; use crypto_rest_client::{fetch_l2_snapshot, fetch_open_interest, DeribitRestClient}; use serde_json::Value; use std::collections::HashMap; use test_case::test_case; #[test_case("BTC-PERPETUAL")] #[test_case("BTC-30DEC22")] #[test_case("BTC-29JUL22-20000-C")] fn test_trades(symbol: &str) { let text = DeribitRestClient::fetch_trades(symbol).unwrap(); let obj = serde_json::from_str::>(&text).unwrap(); let result = obj.get("result").unwrap().as_object().unwrap(); assert!(result.get("trades").unwrap().is_array()); } #[test_case(MarketType::InverseSwap, "BTC-PERPETUAL")] #[test_case(MarketType::InverseFuture, "BTC-30DEC22")] #[test_case(MarketType::EuropeanOption, "BTC-29JUL22-20000-C")] fn test_l2_snapshot(market_type: MarketType, symbol: &str) { let text = fetch_l2_snapshot("deribit", market_type, symbol, Some(3)).unwrap(); let obj = serde_json::from_str::>(&text).unwrap(); let result = obj.get("result").unwrap().as_object().unwrap(); assert!(result.get("asks").unwrap().is_array()); assert!(result.get("bids").unwrap().is_array()) } #[test_case(MarketType::InverseFuture)] #[test_case(MarketType::InverseSwap)] #[test_case(MarketType::EuropeanOption)] fn test_open_interest(market_type: MarketType) { let text = fetch_open_interest("deribit", market_type, None).unwrap(); for line in text.lines() { let obj = serde_json::from_str::>(line).unwrap(); let arr = obj.get("result").unwrap().as_array().unwrap(); assert!(!arr.is_empty()); } } ================================================ FILE: crypto-rest-client/tests/dydx.rs ================================================ use crypto_market_type::MarketType; use crypto_rest_client::{fetch_l2_snapshot, fetch_open_interest}; use serde_json::Value; use std::collections::HashMap; use test_case::test_case; #[test_case(MarketType::LinearSwap, "BTC-USD")] fn test_l2_snapshot(market_type: MarketType, symbol: &str) { let text = fetch_l2_snapshot("dydx", market_type, symbol, Some(3)).unwrap(); let obj = serde_json::from_str::>(&text).unwrap(); let asks = obj.get("asks").unwrap().as_array().unwrap(); let bids = obj.get("bids").unwrap().as_array().unwrap(); assert!(!asks.is_empty()); assert!(!bids.is_empty()); } #[test_case(MarketType::LinearSwap)] fn test_open_interest(market_type: MarketType) { let text = fetch_open_interest("dydx", market_type, None).unwrap(); println!("{text}"); let obj = serde_json::from_str::>(&text).unwrap(); assert!(obj.contains_key("markets")); } ================================================ FILE: crypto-rest-client/tests/ftx.rs ================================================ use std::collections::HashMap; use crypto_market_type::MarketType; use crypto_rest_client::{fetch_l2_snapshot, fetch_open_interest}; use serde_json::Value; use test_case::test_case; #[test_case(MarketType::Spot, "BTC/USD")] #[test_case(MarketType::LinearSwap, "BTC-PERP")] #[test_case(MarketType::LinearFuture, "BTC-1230")] #[test_case(MarketType::Move, "BTC-MOVE-2022Q4")] #[test_case(MarketType::BVOL, "BVOL/USD")] fn test_l2_snapshot(market_type: MarketType, symbol: &str) { let text = fetch_l2_snapshot("ftx", market_type, symbol, Some(3)).unwrap(); let obj = serde_json::from_str::>(&text).unwrap(); let result = obj.get("result").unwrap().as_object().unwrap(); assert!(result.get("asks").unwrap().is_array()); assert!(result.get("bids").unwrap().is_array()) } #[test] fn test_open_interest() { let text = fetch_open_interest("ftx", MarketType::Unknown, None).unwrap(); let obj = serde_json::from_str::>(&text).unwrap(); let result = obj.get("result").unwrap().as_array().unwrap(); assert!(!result.is_empty()) } ================================================ FILE: crypto-rest-client/tests/gate.rs ================================================ use crypto_market_type::MarketType; use crypto_rest_client::{fetch_l2_snapshot, fetch_open_interest}; use serde_json::Value; use std::collections::HashMap; use test_case::test_case; #[test_case(MarketType::Spot, "BTC_USDT")] #[test_case(MarketType::InverseSwap, "BTC_USD")] #[test_case(MarketType::LinearSwap, "BTC_USDT")] #[test_case(MarketType::InverseFuture, "BTC_USD_20221230")] #[test_case(MarketType::LinearFuture, "BTC_USDT_20221230")] fn test_l2_snapshot(market_type: MarketType, symbol: &str) { let text = fetch_l2_snapshot("gate", market_type, symbol, Some(3)).unwrap(); let obj = serde_json::from_str::>(&text).unwrap(); let asks = obj.get("asks").unwrap().as_array().unwrap(); assert!(!asks.is_empty()); let bids = obj.get("bids").unwrap().as_array().unwrap(); assert!(!bids.is_empty()); } #[test_case(MarketType::InverseSwap, "BTC_USD")] #[test_case(MarketType::LinearSwap, "BTC_USDT")] fn test_open_interest(market_type: MarketType, symbol: &str) { let text = fetch_open_interest("gate", market_type, Some(symbol)).unwrap(); let arr = serde_json::from_str::>(&text).unwrap(); assert!(!arr.is_empty()); } ================================================ FILE: crypto-rest-client/tests/huobi.rs ================================================ use crypto_market_type::MarketType; use crypto_rest_client::{fetch_l2_snapshot, fetch_open_interest}; use serde_json::Value; use std::collections::HashMap; use test_case::test_case; #[test_case(MarketType::Spot, "btcusdt")] #[test_case(MarketType::InverseFuture, "BTC_CQ")] #[test_case(MarketType::InverseSwap, "BTC-USD")] #[test_case(MarketType::LinearSwap, "BTC-USDT")] #[test_case(MarketType::EuropeanOption, "BTC-USDT-210625-P-27000"; "inconclusive")] fn test_l2_snapshot(market_type: MarketType, symbol: &str) { let text = fetch_l2_snapshot("huobi", market_type, symbol, Some(3)).unwrap(); assert!(text.starts_with('{')); let obj = serde_json::from_str::>(&text).unwrap(); assert_eq!("ok", obj.get("status").unwrap().as_str().unwrap()); let bids = obj.get("tick").unwrap().as_object().unwrap().get("bids").unwrap().as_array().unwrap(); assert!(!bids.is_empty()); let asks = obj.get("tick").unwrap().as_object().unwrap().get("asks").unwrap().as_array().unwrap(); assert!(!asks.is_empty()); } #[test_case(MarketType::InverseFuture)] #[test_case(MarketType::InverseSwap)] #[test_case(MarketType::LinearSwap)] fn test_open_interest(market_type: MarketType) { let text = fetch_open_interest("huobi", market_type, None).unwrap(); let obj = serde_json::from_str::>(&text).unwrap(); let arr = obj.get("data").unwrap().as_array().unwrap(); assert!(!arr.is_empty()); } #[cfg(test)] mod huobi_spot { use crypto_rest_client::HuobiSpotRestClient; #[test] fn test_trades() { let text = HuobiSpotRestClient::fetch_trades("btcusdt").unwrap(); assert!(text.starts_with('{')); } } #[cfg(test)] mod huobi_future { use crypto_rest_client::HuobiFutureRestClient; #[test] fn test_trades() { let text = HuobiFutureRestClient::fetch_trades("BTC_CQ").unwrap(); assert!(text.starts_with('{')); } } #[cfg(test)] mod huobi_linear_swap { use crypto_rest_client::HuobiLinearSwapRestClient; #[test] fn test_trades() { let text = HuobiLinearSwapRestClient::fetch_trades("BTC-USDT").unwrap(); assert!(text.starts_with('{')); } } #[cfg(test)] mod huobi_inverse_swap { use crypto_rest_client::HuobiInverseSwapRestClient; #[test] fn test_trades() { let text = HuobiInverseSwapRestClient::fetch_trades("BTC-USD").unwrap(); assert!(text.starts_with('{')); } } #[cfg(test)] mod huobi_option { use crypto_rest_client::HuobiOptionRestClient; #[test] #[ignore] fn test_trades() { let text = HuobiOptionRestClient::fetch_trades("BTC-USDT-210625-P-27000").unwrap(); assert!(text.starts_with('{')); } } ================================================ FILE: crypto-rest-client/tests/kraken.rs ================================================ use std::collections::HashMap; use crypto_market_type::MarketType; use crypto_rest_client::{fetch_l2_snapshot, KrakenSpotRestClient}; use serde_json::Value; use test_case::test_case; #[test] fn test_trades() { let text = KrakenSpotRestClient::fetch_trades("XBTUSD", None).unwrap(); let obj = serde_json::from_str::>(&text).unwrap(); assert!(obj.get("error").unwrap().as_array().unwrap().is_empty()); let text = KrakenSpotRestClient::fetch_trades("XBT/USD", None).unwrap(); let obj = serde_json::from_str::>(&text).unwrap(); assert!(obj.get("error").unwrap().as_array().unwrap().is_empty()); } #[test] fn test_restful_accepts_two_symbols() { let text = fetch_l2_snapshot("kraken", MarketType::Spot, "XBTUSD", Some(3)).unwrap(); let obj = serde_json::from_str::>(&text).unwrap(); assert!(obj.get("error").unwrap().as_array().unwrap().is_empty()); let text = fetch_l2_snapshot("kraken", MarketType::Spot, "XBT/USD", Some(3)).unwrap(); let obj = serde_json::from_str::>(&text).unwrap(); assert!(obj.get("error").unwrap().as_array().unwrap().is_empty()); } #[test_case(MarketType::Spot, "XBTUSD")] #[test_case(MarketType::Spot, "XBT/USD")] #[test_case(MarketType::InverseFuture, "FI_XBTUSD_221230")] #[test_case(MarketType::InverseSwap, "PI_XBTUSD")] fn test_l2_snapshot(market_type: MarketType, symbol: &str) { let text = fetch_l2_snapshot("kraken", market_type, symbol, None).unwrap(); let obj = serde_json::from_str::>(&text).unwrap(); if market_type == MarketType::Spot { assert!(obj.get("error").unwrap().as_array().unwrap().is_empty()); } else { assert_eq!(obj.get("result").unwrap().as_str().unwrap(), "success"); } } ================================================ FILE: crypto-rest-client/tests/kucoin.rs ================================================ use crypto_market_type::MarketType; use crypto_rest_client::{fetch_l2_snapshot, fetch_l3_snapshot, fetch_open_interest}; use serde_json::Value; use std::collections::HashMap; use test_case::test_case; #[test_case(MarketType::Spot, "BTC-USDT")] #[test_case(MarketType::InverseFuture, "XBTMZ22")] #[test_case(MarketType::InverseSwap, "XBTUSDM")] #[test_case(MarketType::LinearSwap, "XBTUSDTM")] fn test_l2_snapshot(market_type: MarketType, symbol: &str) { let text = fetch_l2_snapshot("kucoin", market_type, symbol, Some(3)).unwrap(); let obj = serde_json::from_str::>(&text).unwrap(); assert_eq!("200000", obj.get("code").unwrap().as_str().unwrap()); let data = obj.get("data").unwrap().as_object().unwrap(); let asks = data.get("asks").unwrap().as_array().unwrap(); assert!(!asks.is_empty()); let bids = data.get("bids").unwrap().as_array().unwrap(); assert!(!bids.is_empty()); } #[test_case(MarketType::Spot, "BTC-USDT"; "inconclusive")] // TODO: kucoin deprecated level2 and level3 snapshot APIs #[test_case(MarketType::InverseFuture, "XBTMZ22")] #[test_case(MarketType::InverseSwap, "XBTUSDM")] #[test_case(MarketType::LinearSwap, "XBTUSDTM")] fn test_l3_snapshot(market_type: MarketType, symbol: &str) { let text = fetch_l3_snapshot("kucoin", market_type, symbol, Some(3)).unwrap(); let obj = serde_json::from_str::>(&text).unwrap(); assert_eq!("200000", obj.get("code").unwrap().as_str().unwrap()); let data = obj.get("data").unwrap().as_object().unwrap(); let asks = data.get("asks").unwrap().as_array().unwrap(); assert!(!asks.is_empty()); let bids = data.get("bids").unwrap().as_array().unwrap(); assert!(!bids.is_empty()); } #[test_case(MarketType::Unknown)] #[test_case(MarketType::LinearSwap)] fn test_open_interest(market_type: MarketType) { let text = fetch_open_interest("kucoin", market_type, None).unwrap(); let obj = serde_json::from_str::>(&text).unwrap(); let arr = obj.get("data").unwrap().as_array().unwrap(); assert!(!arr.is_empty()); } ================================================ FILE: crypto-rest-client/tests/mexc.rs ================================================ #[cfg(test)] mod mexc_spot { use crypto_rest_client::MexcSpotRestClient; #[test] fn test_trades() { let text = MexcSpotRestClient::fetch_trades("BTC_USDT").unwrap(); assert!(text.starts_with('{')); } #[test] fn test_l2_snapshot() { let text = MexcSpotRestClient::fetch_l2_snapshot("BTC_USDT").unwrap(); assert!(text.starts_with('{')); } } #[cfg(test)] mod mexc_swap { use crypto_market_type::MarketType; use crypto_rest_client::{fetch_l2_snapshot, MexcSwapRestClient}; #[test] fn test_trades() { let text = MexcSwapRestClient::fetch_trades("BTC_USDT").unwrap(); assert!(text.starts_with('{')); } #[test] fn test_l2_snapshot() { let text = fetch_l2_snapshot("mexc", MarketType::LinearSwap, "BTC_USDT", Some(3)).unwrap(); assert!(text.starts_with('{')); } } ================================================ FILE: crypto-rest-client/tests/okx.rs ================================================ use crypto_market_type::MarketType; use crypto_rest_client::{fetch_l2_snapshot, fetch_open_interest}; use serde_json::Value; use std::collections::HashMap; use test_case::test_case; #[test_case(MarketType::Spot, "BTC-USDT")] #[test_case(MarketType::InverseFuture, "BTC-USD-221230")] #[test_case(MarketType::LinearFuture, "BTC-USDT-221230")] #[test_case(MarketType::InverseSwap, "BTC-USD-SWAP")] #[test_case(MarketType::LinearSwap, "BTC-USDT-SWAP")] #[test_case(MarketType::EuropeanOption, "BTC-USD-221230-10000-P")] fn test_l2_snapshot(market_type: MarketType, symbol: &str) { let text = fetch_l2_snapshot("okx", market_type, symbol, Some(3)).unwrap(); assert!(text.starts_with('{')); } #[test_case(MarketType::InverseFuture, "BTC-USD-221230")] #[test_case(MarketType::LinearFuture, "BTC-USDT-221230")] #[test_case(MarketType::InverseSwap, "BTC-USD-SWAP")] #[test_case(MarketType::LinearSwap, "BTC-USDT-SWAP")] #[test_case(MarketType::EuropeanOption, "BTC-USD-221230-10000-P")] fn test_open_interest(market_type: MarketType, symbol: &str) { let text = fetch_open_interest("okx", market_type, Some(symbol)).unwrap(); let json_obj = serde_json::from_str::>(&text).unwrap(); let arr = json_obj.get("data").unwrap().as_array().unwrap(); assert!(!arr.is_empty()); } #[cfg(test)] mod okex_swap { use std::collections::HashMap; use crypto_rest_client::OkxRestClient; use serde_json::Value; #[test] fn test_trades() { let text = OkxRestClient::fetch_trades("BTC-USDT-SWAP").unwrap(); let json_obj = serde_json::from_str::>(&text).unwrap(); let code = json_obj.get("code").unwrap().as_str().unwrap(); assert_eq!("0", code); } #[test] fn test_option_underlying() { let arr = OkxRestClient::fetch_option_underlying().unwrap(); assert!(!arr.is_empty()); } } ================================================ FILE: crypto-rest-client/tests/zb.rs ================================================ use std::collections::HashMap; use crypto_market_type::MarketType; use crypto_rest_client::fetch_l2_snapshot; use serde_json::Value; #[test] fn test_spot_l2_snapshot() { let text = fetch_l2_snapshot("zb", MarketType::Spot, "btc_usdt", Some(3)).unwrap(); let obj = serde_json::from_str::>(&text).unwrap(); assert!(!obj.get("asks").unwrap().as_array().unwrap().is_empty()); assert!(!obj.get("bids").unwrap().as_array().unwrap().is_empty()); } #[test] fn test_swap_l2_snapshot() { let text = fetch_l2_snapshot("zb", MarketType::LinearSwap, "BTC_USDT", Some(3)).unwrap(); let obj = serde_json::from_str::>(&text).unwrap(); assert_eq!(10000, obj["code"].as_i64().unwrap()); let data = obj.get("data").unwrap().as_object().unwrap(); assert!(!data.get("asks").unwrap().as_array().unwrap().is_empty()); assert!(!data.get("bids").unwrap().as_array().unwrap().is_empty()); } ================================================ FILE: crypto-rest-client/tests/zbg.rs ================================================ use std::collections::HashMap; use crypto_market_type::MarketType; use crypto_rest_client::{fetch_l2_snapshot, fetch_open_interest}; use serde_json::Value; use test_case::test_case; #[test_case(MarketType::Spot, "btc_usdt")] #[test_case(MarketType::InverseSwap, "BTC_USD-R")] #[test_case(MarketType::LinearSwap, "BTC_USDT")] fn test_l2_snapshot(market_type: MarketType, symbol: &str) { let text = fetch_l2_snapshot("zbg", market_type, symbol, Some(3)).unwrap(); let obj = serde_json::from_str::>(&text).unwrap(); let code = obj.get("resMsg").unwrap().as_object().unwrap().get("code").unwrap().as_str().unwrap(); if code == "6001" || code == "6020" { // system in maintainance return; } assert_eq!("1", code); let data = obj.get("datas").unwrap().as_object().unwrap(); assert!(data.get("asks").unwrap().as_array().is_some()); assert!(data.get("bids").unwrap().as_array().is_some()); } #[test_case(MarketType::InverseSwap, "BTC_USD-R")] #[test_case(MarketType::LinearSwap, "BTC_USDT")] fn test_open_interest(market_type: MarketType, symbol: &str) { let text = fetch_open_interest("zbg", market_type, Some(symbol)); match text { Ok(text) => { let obj = serde_json::from_str::>(&text).unwrap(); assert!(obj.contains_key("datas")); } Err(e) => { eprintln!("Unable to connect to ZBG API: {e}") } } } ================================================ FILE: crypto-ws-client/Cargo.toml ================================================ [package] name = "crypto-ws-client" version = "4.12.11" authors = ["soulmachine "] edition = "2021" description = "A versatile websocket client that supports many cryptocurrency exchanges." license = "Apache-2.0" repository = "https://github.com/crypto-crawler/crypto-crawler-rs/tree/main/crypto-ws-client" keywords = ["cryptocurrency", "blockchain", "trading", "websocket"] [dependencies] async-trait = "0.1.64" flate2 = "1.0.25" futures-util = "0.3.26" governor = "0.5.1" nonzero_ext = "0.3.0" log = "0.4.17" rand = "0.8.5" reqwest = { version = "0.11.14", features = ["gzip"] } serde_json = "1.0.93" tokio = { version = "1.25.0", features = ["rt-multi-thread", "time", "sync", "macros"] } tokio-tungstenite = { version = "0.18.0", features = ["rustls-tls-native-roots"] } fast-socks5 = "0.8.1" [dev-dependencies] tokio = { version = "1.25.0", features = ["test-util"] } ================================================ FILE: crypto-ws-client/README.md ================================================ # crypto-ws-client [![](https://img.shields.io/github/workflow/status/crypto-crawler/crypto-crawler-rs/CI/main)](https://github.com/crypto-crawler/crypto-crawler-rs/actions?query=branch%3Amain) [![](https://img.shields.io/crates/v/crypto-ws-client.svg)](https://crates.io/crates/crypto-ws-client) [![](https://docs.rs/crypto-ws-client/badge.svg)](https://docs.rs/crypto-ws-client) ========== A versatile websocket client that supports many cryptocurrency exchanges. ## Usage ```rust use crypto_ws_client::{BinanceSpotWSClient, WSClient}; #[tokio::main] async fn main() { let (tx, rx) = std::sync::mpsc::channel(); tokio::task::spawn(async move { let symbols = vec!["BTCUSDT".to_string(), "ETHUSDT".to_string()]; let ws_client = BinanceSpotWSClient::new(tx, None).await; ws_client.subscribe_trade(&symbols).await; // run for 5 seconds let _ = tokio::time::timeout(std::time::Duration::from_secs(5), ws_client.run()).await; ws_client.close(); }); for msg in rx { println!("{}", msg); } } ``` ================================================ FILE: crypto-ws-client/src/clients/binance.rs ================================================ use async_trait::async_trait; use nonzero_ext::nonzero; use std::{collections::HashMap, num::NonZeroU32}; use tokio_tungstenite::tungstenite::Message; use crate::{ common::{ command_translator::CommandTranslator, message_handler::{MessageHandler, MiscMessage}, utils::ensure_frame_size, ws_client_internal::WSClientInternal, }, WSClient, }; use log::*; use serde_json::Value; pub(crate) const EXCHANGE_NAME: &str = "binance"; const SPOT_WEBSOCKET_URL: &str = "wss://stream.binance.com:9443/stream"; const LINEAR_WEBSOCKET_URL: &str = "wss://fstream.binance.com/stream"; const INVERSE_WEBSOCKET_URL: &str = "wss://dstream.binance.com/stream"; // the websocket message size should not exceed 4096 bytes, otherwise // you'll get `code: 3001, reason: illegal request` const WS_FRAME_SIZE: usize = 4096; // WebSocket connections have a limit of 5 incoming messages per second. // // See: // // * https://binance-docs.github.io/apidocs/spot/en/#limits // * https://binance-docs.github.io/apidocs/futures/en/#websocket-market-streams // * https://binance-docs.github.io/apidocs/delivery/en/#websocket-market-streams const UPLINK_LIMIT: (NonZeroU32, std::time::Duration) = (nonzero!(5u32), std::time::Duration::from_secs(1)); // Internal unified client pub struct BinanceWSClient { client: WSClientInternal, translator: BinanceCommandTranslator, } /// Binance Spot market. /// /// * WebSocket API doc: /// * Trading at: pub type BinanceSpotWSClient = BinanceWSClient<'S'>; /// Binance Coin-margined Future and Swap markets. /// /// * WebSocket API doc: /// * Trading at: pub type BinanceInverseWSClient = BinanceWSClient<'I'>; /// Binance USDT-margined Future and Swap markets. /// /// * WebSocket API doc: /// * Trading at: pub type BinanceLinearWSClient = BinanceWSClient<'L'>; impl BinanceWSClient { pub async fn new(tx: std::sync::mpsc::Sender, url: Option<&str>) -> Self { let real_url = match url { Some(endpoint) => endpoint, None => { if MARKET_TYPE == 'S' { SPOT_WEBSOCKET_URL } else if MARKET_TYPE == 'I' { INVERSE_WEBSOCKET_URL } else if MARKET_TYPE == 'L' { LINEAR_WEBSOCKET_URL } else { panic!("Unknown market type {MARKET_TYPE}"); } } }; BinanceWSClient { client: WSClientInternal::connect( EXCHANGE_NAME, real_url, BinanceMessageHandler {}, Some(UPLINK_LIMIT), tx, ) .await, translator: BinanceCommandTranslator { market_type: MARKET_TYPE }, } } } #[async_trait] impl WSClient for BinanceWSClient { async fn subscribe_trade(&self, symbols: &[String]) { let topics = symbols .iter() .map(|symbol| ("aggTrade".to_string(), symbol.to_string())) .collect::>(); self.subscribe(&topics).await; } async fn subscribe_orderbook(&self, symbols: &[String]) { let topics = symbols .iter() .map(|symbol| ("depth@100ms".to_string(), symbol.to_string())) .collect::>(); self.subscribe(&topics).await; } async fn subscribe_orderbook_topk(&self, symbols: &[String]) { let topics = symbols .iter() .map(|symbol| ("depth20".to_string(), symbol.to_string())) .collect::>(); self.subscribe(&topics).await; } async fn subscribe_l3_orderbook(&self, _symbols: &[String]) { panic!("{EXCHANGE_NAME} does NOT have the level3 websocket channel"); } async fn subscribe_ticker(&self, symbols: &[String]) { let topics = symbols .iter() .map(|symbol| ("ticker".to_string(), symbol.to_string())) .collect::>(); self.subscribe(&topics).await; } async fn subscribe_bbo(&self, symbols: &[String]) { let topics = symbols .iter() .map(|symbol| ("bookTicker".to_string(), symbol.to_string())) .collect::>(); self.subscribe(&topics).await; } async fn subscribe_candlestick(&self, symbol_interval_list: &[(String, usize)]) { let commands = self.translator.translate_to_candlestick_commands(true, symbol_interval_list); self.client.send(&commands).await; } async fn subscribe(&self, topics: &[(String, String)]) { let commands = self.translator.translate_to_commands(true, topics); self.client.send(&commands).await; } async fn unsubscribe(&self, topics: &[(String, String)]) { let commands = self.translator.translate_to_commands(false, topics); self.client.send(&commands).await; } async fn send(&self, commands: &[String]) { self.client.send(commands).await; } async fn run(&self) { self.client.run().await; } async fn close(&self) { self.client.close().await; } } struct BinanceMessageHandler {} struct BinanceCommandTranslator { market_type: char, } impl BinanceCommandTranslator { fn topics_to_command(topics: &[(String, String)], subscribe: bool) -> String { let raw_topics = topics .iter() .map(|(topic, symbol)| format!("{}@{}", symbol.to_lowercase(), topic)) .collect::>(); format!( r#"{{"id":9527,"method":"{}","params":{}}}"#, if subscribe { "SUBSCRIBE" } else { "UNSUBSCRIBE" }, serde_json::to_string(&raw_topics).unwrap() ) } // see https://binance-docs.github.io/apidocs/futures/en/#kline-candlestick-streams fn to_candlestick_raw_channel(interval: usize) -> String { let interval_str = match interval { 60 => "1m", 180 => "3m", 300 => "5m", 900 => "15m", 1800 => "30m", 3600 => "1h", 7200 => "2h", 14400 => "4h", 21600 => "6h", 28800 => "8h", 43200 => "12h", 86400 => "1d", 259200 => "3d", 604800 => "1w", 2592000 => "1M", _ => panic!("Binance has intervals 1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,1M"), }; format!("kline_{interval_str}") } } impl MessageHandler for BinanceMessageHandler { fn handle_message(&mut self, msg: &str) -> MiscMessage { let resp = serde_json::from_str::>(msg); if resp.is_err() { error!("{} is not a JSON string, {}", msg, EXCHANGE_NAME); return MiscMessage::Other; } let obj = resp.unwrap(); if obj.contains_key("error") { panic!("Received {msg} from {EXCHANGE_NAME}"); } else if obj.contains_key("stream") && obj.contains_key("data") { MiscMessage::Normal } else { if let Some(result) = obj.get("result") { if serde_json::Value::Null != *result { panic!("Received {msg} from {EXCHANGE_NAME}"); } else { info!("Received {} from {}", msg, EXCHANGE_NAME); } } else { warn!("Received {} from {}", msg, EXCHANGE_NAME); } MiscMessage::Other } } fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)> { // https://binance-docs.github.io/apidocs/spot/en/#websocket-market-streams // https://binance-docs.github.io/apidocs/futures/en/#websocket-market-streams // https://binance-docs.github.io/apidocs/delivery/en/#websocket-market-streams // The websocket server will send a ping frame every 3 minutes. If the websocket // server does not receive a pong frame back from the connection within // a 10 minute period, the connection will be disconnected. Unsolicited // pong frames are allowed. Send unsolicited pong frames per 3 minutes Some((Message::Pong(Vec::new()), 180)) } } impl CommandTranslator for BinanceCommandTranslator { fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec { let max_num_topics = if self.market_type == 'S' { // https://binance-docs.github.io/apidocs/spot/en/#websocket-limits 1024 } else { // https://binance-docs.github.io/apidocs/futures/en/#websocket-market-streams // https://binance-docs.github.io/apidocs/delivery/en/#websocket-market-streams 200 }; ensure_frame_size( topics, subscribe, Self::topics_to_command, WS_FRAME_SIZE, Some(max_num_topics), ) } fn translate_to_candlestick_commands( &self, subscribe: bool, symbol_interval_list: &[(String, usize)], ) -> Vec { let topics = symbol_interval_list .iter() .map(|(symbol, interval)| { let channel = Self::to_candlestick_raw_channel(*interval); (channel, symbol.to_lowercase()) }) .collect::>(); self.translate_to_commands(subscribe, &topics) } } #[cfg(test)] mod tests { use crate::common::command_translator::CommandTranslator; #[test] fn test_one_topic() { let translator = super::BinanceCommandTranslator { market_type: 'S' }; let commands = translator .translate_to_commands(true, &[("aggTrade".to_string(), "BTCUSDT".to_string())]); assert_eq!(1, commands.len()); assert_eq!( r#"{"id":9527,"method":"SUBSCRIBE","params":["btcusdt@aggTrade"]}"#, commands[0] ); } #[test] fn test_two_topics() { let translator = super::BinanceCommandTranslator { market_type: 'S' }; let commands = translator.translate_to_commands( true, &[ ("aggTrade".to_string(), "BTCUSDT".to_string()), ("ticker".to_string(), "BTCUSDT".to_string()), ], ); assert_eq!(1, commands.len()); assert_eq!( r#"{"id":9527,"method":"SUBSCRIBE","params":["btcusdt@aggTrade","btcusdt@ticker"]}"#, commands[0] ); } } ================================================ FILE: crypto-ws-client/src/clients/binance_option.rs ================================================ use async_trait::async_trait; use std::collections::HashMap; use tokio_tungstenite::tungstenite::Message; use crate::{ clients::common_traits::{ Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO, }, common::{ command_translator::CommandTranslator, message_handler::{MessageHandler, MiscMessage}, ws_client_internal::WSClientInternal, }, WSClient, }; use log::*; use serde_json::Value; pub(crate) const EXCHANGE_NAME: &str = "binance"; pub(super) const WEBSOCKET_URL: &str = "wss://stream.opsnest.com/stream"; /// Binance Option market /// /// * WebSocket API doc: /// * Trading at: pub struct BinanceOptionWSClient { client: WSClientInternal, translator: BinanceOptionCommandTranslator, } impl_new_constructor!( BinanceOptionWSClient, EXCHANGE_NAME, WEBSOCKET_URL, BinanceOptionMessageHandler {}, BinanceOptionCommandTranslator {} ); impl_trait!(Trade, BinanceOptionWSClient, subscribe_trade, "trade"); impl_trait!(Ticker, BinanceOptionWSClient, subscribe_ticker, "ticker"); impl_trait!(BBO, BinanceOptionWSClient, subscribe_bbo, "bookTicker"); #[rustfmt::skip] impl_trait!(OrderBook, BinanceOptionWSClient, subscribe_orderbook, "depth@100ms"); #[rustfmt::skip] impl_trait!(OrderBookTopK, BinanceOptionWSClient, subscribe_orderbook_topk, "depth10"); impl_candlestick!(BinanceOptionWSClient); panic_l3_orderbook!(BinanceOptionWSClient); impl_ws_client_trait!(BinanceOptionWSClient); struct BinanceOptionMessageHandler {} struct BinanceOptionCommandTranslator {} impl BinanceOptionCommandTranslator { fn topics_to_command(topics: &[(String, String)], subscribe: bool) -> String { let raw_topics = topics .iter() .map(|(topic, symbol)| format!("{symbol}@{topic}")) .collect::>(); format!( r#"{{"id":9527,"method":"{}","params":{}}}"#, if subscribe { "SUBSCRIBE" } else { "UNSUBSCRIBE" }, serde_json::to_string(&raw_topics).unwrap() ) } // see https://binance-docs.github.io/apidocs/voptions/en/#payload-candle fn to_candlestick_raw_channel(interval: usize) -> String { let interval_str = match interval { 60 => "1m", 300 => "5m", 900 => "15m", 1800 => "30m", 3600 => "1h", 14400 => "4h", 86400 => "1d", 604800 => "1w", _ => panic!("Binance Option has intervals 1m,5m,15m,30m,1h4h,1d,1w"), }; format!("kline_{interval_str}") } } impl MessageHandler for BinanceOptionMessageHandler { fn handle_message(&mut self, msg: &str) -> MiscMessage { if msg == r#"{"id":9527}"# { return MiscMessage::Other; } else if msg == r#"{"event":"pong"}"# { return MiscMessage::Pong; } let resp = serde_json::from_str::>(msg); if resp.is_err() { error!("{} is not a JSON string, {}", msg, EXCHANGE_NAME); return MiscMessage::Other; } let obj = resp.unwrap(); if obj.contains_key("code") { panic!("Received {msg} from {EXCHANGE_NAME}"); } if let Some(result) = obj.get("result") { if serde_json::Value::Null == *result { return MiscMessage::Other; } } if !obj.contains_key("stream") || !obj.contains_key("data") { warn!("Received {} from {}", msg, EXCHANGE_NAME); return MiscMessage::Other; } MiscMessage::Normal } fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)> { // https://binance-docs.github.io/apidocs/voptions/en/#push-websocket-account-info // The client will send a ping frame every 2 minutes. If the websocket server // does not receive a ping frame back from the connection within a 2 // minute period, the connection will be disconnected. Unsolicited ping // frames are allowed. Some((Message::Text(r#"{"event":"ping"}"#.to_string()), 120)) } } impl CommandTranslator for BinanceOptionCommandTranslator { fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec { let command = Self::topics_to_command(topics, subscribe); vec![command] } fn translate_to_candlestick_commands( &self, subscribe: bool, symbol_interval_list: &[(String, usize)], ) -> Vec { let topics = symbol_interval_list .iter() .map(|(symbol, interval)| { let channel = Self::to_candlestick_raw_channel(*interval); (channel, symbol.to_string()) }) .collect::>(); self.translate_to_commands(subscribe, &topics) } } #[cfg(test)] mod tests { use crate::common::command_translator::CommandTranslator; #[test] fn test_one_topic() { let translator = super::BinanceOptionCommandTranslator {}; let commands = translator.translate_to_commands( true, &[("trade".to_string(), "BTC-220429-50000-C".to_string())], ); assert_eq!(1, commands.len()); assert_eq!( r#"{"id":9527,"method":"SUBSCRIBE","params":["BTC-220429-50000-C@trade"]}"#, commands[0] ); } #[test] fn test_two_topics() { let translator = super::BinanceOptionCommandTranslator {}; let commands = translator.translate_to_commands( true, &[ ("trade".to_string(), "BTC-220429-50000-C".to_string()), ("ticker".to_string(), "BTC-220429-50000-C".to_string()), ], ); assert_eq!(1, commands.len()); assert_eq!( r#"{"id":9527,"method":"SUBSCRIBE","params":["BTC-220429-50000-C@trade","BTC-220429-50000-C@ticker"]}"#, commands[0] ); } } ================================================ FILE: crypto-ws-client/src/clients/bitfinex.rs ================================================ use async_trait::async_trait; use std::{ collections::{BTreeMap, HashMap}, time::Duration, }; use tokio_tungstenite::tungstenite::Message; use crate::{ clients::common_traits::{ Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO, }, common::{ command_translator::CommandTranslator, message_handler::{MessageHandler, MiscMessage}, ws_client_internal::WSClientInternal, }, WSClient, }; use log::*; use serde_json::Value; pub(super) const EXCHANGE_NAME: &str = "bitfinex"; const WEBSOCKET_URL: &str = "wss://api-pub.bitfinex.com/ws/2"; /// The WebSocket client for Bitfinex, including all markets. /// /// * WebSocket API doc: /// * Spot: /// * Swap: /// * Funding: pub struct BitfinexWSClient { client: WSClientInternal, translator: BitfinexCommandTranslator, // used by close() and run() } impl_new_constructor!( BitfinexWSClient, EXCHANGE_NAME, WEBSOCKET_URL, BitfinexMessageHandler { channel_id_meta: HashMap::new() }, BitfinexCommandTranslator {} ); impl_trait!(Trade, BitfinexWSClient, subscribe_trade, "trades"); impl_trait!(Ticker, BitfinexWSClient, subscribe_ticker, "ticker"); impl_candlestick!(BitfinexWSClient); panic_bbo!(BitfinexWSClient); panic_l2_topk!(BitfinexWSClient); #[async_trait] impl OrderBook for BitfinexWSClient { async fn subscribe_orderbook(&self, symbols: &[String]) { let commands = symbols .iter() .map(|symbol| { format!(r#"{{"event": "subscribe","channel": "book","symbol": "{symbol}","prec": "P0","frec": "F0","len":25}}"#, ) }) .collect::>(); self.send(&commands).await; } } #[async_trait] impl Level3OrderBook for BitfinexWSClient { async fn subscribe_l3_orderbook(&self, symbols: &[String]) { let commands = symbols .iter() .map(|symbol| { format!(r#"{{"event": "subscribe","channel": "book","symbol": "{symbol}","prec": "R0","len": 250}}"#, ) }) .collect::>(); self.send(&commands).await; } } impl_ws_client_trait!(BitfinexWSClient); struct BitfinexMessageHandler { channel_id_meta: HashMap, // CHANNEL_ID information } struct BitfinexCommandTranslator {} impl BitfinexCommandTranslator { fn topic_to_command(channel: &str, symbol: &str, subscribe: bool) -> String { format!( r#"{{"event": "{}", "channel": "{}", "symbol": "{}"}}"#, if subscribe { "subscribe" } else { "unsubscribe" }, channel, symbol ) } fn to_candlestick_command(symbol: &str, interval: usize, subscribe: bool) -> String { let interval_str = match interval { 60 => "1m", 300 => "5m", 900 => "15m", 1800 => "30m", 3600 => "1h", 10800 => "3h", 21600 => "6h", 43200 => "12h", 86400 => "1D", 604800 => "7D", 1209600 => "14D", 2592000 => "1M", _ => panic!("Bitfinex available intervals 1m,5m,15m,30m,1h,3h,6h,12h,1D,7D,14D,1M"), }; format!( r#"{{"event": "{}","channel": "candles","key": "trade:{}:{}"}}"#, if subscribe { "subscribe" } else { "unsubscribe" }, interval_str, symbol ) } } impl MessageHandler for BitfinexMessageHandler { fn handle_message(&mut self, txt: &str) -> MiscMessage { if txt.starts_with('{') { let obj = serde_json::from_str::>(txt).unwrap(); let event = obj.get("event").unwrap().as_str().unwrap(); match event { "error" => { let code = obj.get("code").unwrap().as_i64().unwrap(); match code { 10301 | 10401 => { // 10301: Already subscribed // 10401: Not subscribed // 10000: Unknown event warn!("{} from {}", txt, EXCHANGE_NAME); } 10300 | 10400 | 10302 => { // 10300, 10400:Subscription failed // 10302: Unknown channel // 10001: Unknown pair // 10305: Reached limit of open channels error!("{} from {}", txt, EXCHANGE_NAME); panic!("{txt} from {EXCHANGE_NAME}"); } _ => warn!("{} from {}", txt, EXCHANGE_NAME), } MiscMessage::Other } "info" => { if obj.get("version").is_some() { // 1 for operative, 0 for maintenance let status = obj .get("platform") .unwrap() .as_object() .unwrap() .get("status") .unwrap() .as_i64() .unwrap(); if status == 0 { std::thread::sleep(Duration::from_secs(15)); MiscMessage::Reconnect } else { MiscMessage::Other } } else { let code = obj.get("code").unwrap().as_i64().unwrap(); match code { 20051 => { // Stop/Restart Websocket Server (please reconnect) // self.reconnect(); error!("Stop/Restart Websocket Server, exiting now..."); MiscMessage::Reconnect // fail fast, pm2 will restart } 20060 => { // Entering in Maintenance mode. Please pause any activity and // resume after receiving the info // message 20061 (it should take 120 seconds // at most). std::thread::sleep(Duration::from_secs(15)); MiscMessage::Other } 20061 => { // Maintenance ended. You can resume normal activity. It is advised // to unsubscribe/subscribe again all channels. MiscMessage::Reconnect } _ => { info!("{} from {}", txt, EXCHANGE_NAME); MiscMessage::Other } } } } "pong" => { debug!("{} from {}", txt, EXCHANGE_NAME); MiscMessage::Pong } "conf" => { warn!("{} from {}", txt, EXCHANGE_NAME); MiscMessage::Other } "subscribed" => { let mut obj_sorted = BTreeMap::::new(); for (key, value) in obj.iter() { obj_sorted.insert(key.to_string(), value.clone()); } let chan_id = obj.get("chanId").unwrap().as_i64().unwrap(); obj_sorted.remove("event"); obj_sorted.remove("chanId"); obj_sorted.remove("pair"); self.channel_id_meta .insert(chan_id, serde_json::to_string(&obj_sorted).unwrap()); MiscMessage::Other } "unsubscribed" => { let chan_id = obj.get("chanId").unwrap().as_i64().unwrap(); self.channel_id_meta.remove(&chan_id); MiscMessage::Other } _ => MiscMessage::Other, } } else { debug_assert!(txt.starts_with('[')); let arr = serde_json::from_str::>(txt).unwrap(); if arr.is_empty() { MiscMessage::Other // ignore empty array } else if arr.len() == 2 && arr[1].as_str().unwrap_or("null") == "hb" { // If there is no activity in the channel for 15 seconds, the Websocket server // will send you a heartbeat message in this format. // see MiscMessage::WebSocket(Message::Text(r#"{"event":"ping"}"#.to_string())) } else { // replace CHANNEL_ID with meta info let i = txt.find(',').unwrap(); // first comma, for example, te, tu, see https://blog.bitfinex.com/api/websocket-api-update/ let channel_id = (txt[1..i]).parse::().unwrap(); if let Some(channel_info) = self.channel_id_meta.get(&channel_id) { let new_txt = format!("[{}{}", channel_info, &txt[i..]); MiscMessage::Mutated(new_txt) } else { MiscMessage::Other } } } } fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)> { // If there is no activity in the channel for 15 seconds, the Websocket server // will send you a heartbeat message, see https://docs.bitfinex.com/docs/ws-general#heartbeating None } } impl CommandTranslator for BitfinexCommandTranslator { fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec { topics .iter() .map(|(channel, symbol)| Self::topic_to_command(channel, symbol, subscribe)) .collect() } fn translate_to_candlestick_commands( &self, subscribe: bool, symbol_interval_list: &[(String, usize)], ) -> Vec { symbol_interval_list .iter() .map(|(symbol, interval)| Self::to_candlestick_command(symbol, *interval, subscribe)) .collect::>() } } #[cfg(test)] mod tests { use crate::common::command_translator::CommandTranslator; #[test] fn test_spot_command() { let translator = super::BitfinexCommandTranslator {}; let commands = translator .translate_to_commands(true, &[("trades".to_string(), "tBTCUSD".to_string())]); assert_eq!(1, commands.len()); assert_eq!( r#"{"event": "subscribe", "channel": "trades", "symbol": "tBTCUSD"}"#, commands[0] ); } #[test] fn test_swap_command() { let translator = super::BitfinexCommandTranslator {}; let commands = translator .translate_to_commands(true, &[("trades".to_string(), "tBTCF0:USTF0".to_string())]); assert_eq!(1, commands.len()); assert_eq!( r#"{"event": "subscribe", "channel": "trades", "symbol": "tBTCF0:USTF0"}"#, commands[0] ); } } ================================================ FILE: crypto-ws-client/src/clients/bitget/bitget_spot.rs ================================================ use async_trait::async_trait; use crate::{ clients::common_traits::{ Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO, }, common::{command_translator::CommandTranslator, ws_client_internal::WSClientInternal}, WSClient, }; use super::{ utils::{BitgetCommandTranslator, BitgetMessageHandler, UPLINK_LIMIT}, EXCHANGE_NAME, }; const WEBSOCKET_URL: &str = "wss://ws.bitget.com/spot/v1/stream"; /// The WebSocket client for Bitget Spot market. /// /// * WebSocket API doc: /// * Trading at: pub struct BitgetSpotWSClient { client: WSClientInternal, translator: BitgetCommandTranslator<'S'>, } impl BitgetSpotWSClient { pub async fn new(tx: std::sync::mpsc::Sender, url: Option<&str>) -> Self { let real_url = match url { Some(endpoint) => endpoint, None => WEBSOCKET_URL, }; BitgetSpotWSClient { client: WSClientInternal::connect( EXCHANGE_NAME, real_url, BitgetMessageHandler {}, Some(UPLINK_LIMIT), tx, ) .await, translator: BitgetCommandTranslator::<'S'> {}, } } } impl_trait!(Trade, BitgetSpotWSClient, subscribe_trade, "trade"); #[rustfmt::skip] impl_trait!(OrderBookTopK, BitgetSpotWSClient, subscribe_orderbook_topk, "books15"); impl_trait!(OrderBook, BitgetSpotWSClient, subscribe_orderbook, "books"); impl_trait!(Ticker, BitgetSpotWSClient, subscribe_ticker, "ticker"); impl_candlestick!(BitgetSpotWSClient); panic_bbo!(BitgetSpotWSClient); panic_l3_orderbook!(BitgetSpotWSClient); impl_ws_client_trait!(BitgetSpotWSClient); ================================================ FILE: crypto-ws-client/src/clients/bitget/bitget_swap.rs ================================================ use async_trait::async_trait; use crate::{ clients::common_traits::{ Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO, }, common::{command_translator::CommandTranslator, ws_client_internal::WSClientInternal}, WSClient, }; use super::{ utils::{BitgetCommandTranslator, BitgetMessageHandler, UPLINK_LIMIT}, EXCHANGE_NAME, }; const WEBSOCKET_URL: &str = "wss://ws.bitget.com/mix/v1/stream"; /// The WebSocket client for Bitget swap markets. /// /// * WebSocket API doc: /// * Trading at: pub struct BitgetSwapWSClient { client: WSClientInternal, translator: BitgetCommandTranslator<'M'>, } impl BitgetSwapWSClient { pub async fn new(tx: std::sync::mpsc::Sender, url: Option<&str>) -> Self { let real_url = match url { Some(endpoint) => endpoint, None => WEBSOCKET_URL, }; BitgetSwapWSClient { client: WSClientInternal::connect( EXCHANGE_NAME, real_url, BitgetMessageHandler {}, Some(UPLINK_LIMIT), tx, ) .await, translator: BitgetCommandTranslator::<'M'> {}, } } } impl_trait!(Trade, BitgetSwapWSClient, subscribe_trade, "trade"); #[rustfmt::skip] impl_trait!(OrderBookTopK, BitgetSwapWSClient, subscribe_orderbook_topk, "books15"); impl_trait!(OrderBook, BitgetSwapWSClient, subscribe_orderbook, "books"); impl_trait!(Ticker, BitgetSwapWSClient, subscribe_ticker, "ticker"); impl_candlestick!(BitgetSwapWSClient); panic_bbo!(BitgetSwapWSClient); panic_l3_orderbook!(BitgetSwapWSClient); impl_ws_client_trait!(BitgetSwapWSClient); ================================================ FILE: crypto-ws-client/src/clients/bitget/mod.rs ================================================ mod bitget_spot; mod bitget_swap; mod utils; pub use bitget_spot::BitgetSpotWSClient; pub use bitget_swap::BitgetSwapWSClient; pub(super) const EXCHANGE_NAME: &str = "bitget"; ================================================ FILE: crypto-ws-client/src/clients/bitget/utils.rs ================================================ use nonzero_ext::nonzero; use std::{ collections::{BTreeMap, HashMap}, num::NonZeroU32, }; use tokio_tungstenite::tungstenite::Message; use log::*; use serde_json::Value; use crate::common::{ command_translator::CommandTranslator, message_handler::{MessageHandler, MiscMessage}, utils::ensure_frame_size, }; pub(crate) const EXCHANGE_NAME: &str = "bitget"; // The total length of multiple channel can not exceeds 4096 bytes, see: // * https://bitgetlimited.github.io/apidoc/en/mix/#subscribe // * https://bitgetlimited.github.io/apidoc/en/spot/#subscribe const WS_FRAME_SIZE: usize = 4096; // Subscription limit: 240 times per hour, see: // * https://bitgetlimited.github.io/apidoc/en/mix/#connect // * https://bitgetlimited.github.io/apidoc/en/spot/#connect pub(super) const UPLINK_LIMIT: (NonZeroU32, std::time::Duration) = (nonzero!(240u32), std::time::Duration::from_secs(3600)); // MARKET_TYPE: S for SP, M for MC pub(super) struct BitgetMessageHandler {} pub(super) struct BitgetCommandTranslator {} impl BitgetCommandTranslator { // doc: https://bitgetlimited.github.io/apidoc/en/spot/#subscribe fn topics_to_command(topics: &[(String, String)], subscribe: bool) -> String { let arr = topics .iter() .map(|t| { let mut map = BTreeMap::new(); let (channel, symbol) = t; // websocket doesn't recognize SPBL, DMCBL and UMCBL suffixes let symbol = if let Some(x) = symbol.strip_suffix("_SPBL") { x } else if let Some(x) = symbol.strip_suffix("_DMCBL") { x } else if let Some(x) = symbol.strip_suffix("_UMCBL") { x } else { symbol }; map.insert( "instType".to_string(), (if MARKET_TYPE == 'S' { "SP" } else { "MC" }).to_string(), ); map.insert("channel".to_string(), channel.to_string()); map.insert("instId".to_string(), symbol.to_string()); map }) .collect::>>(); format!( r#"{{"op":"{}","args":{}}}"#, if subscribe { "subscribe" } else { "unsubscribe" }, serde_json::to_string(&arr).unwrap(), ) } // https://bitgetlimited.github.io/apidoc/en/spot/#candlesticks-channel // https://bitgetlimited.github.io/apidoc/en/mix/#candlesticks-channel fn to_candlestick_raw_channel(interval: usize) -> &'static str { match interval { 60 => "candle1m", 300 => "candle5m", 900 => "candle15m", 1800 => "candle30m", 3600 => "candle1H", 14400 => "candle4H", 43200 => "candle12H", 86400 => "candle1D", 604800 => "candle1W", _ => panic!("Invalid Bitget candlestick interval {interval}"), } } } impl MessageHandler for BitgetMessageHandler { // the logic is almost the same with OKX fn handle_message(&mut self, msg: &str) -> MiscMessage { if msg == "pong" { // see https://bitgetlimited.github.io/apidoc/en/spot/#connect return MiscMessage::Pong; } let resp = serde_json::from_str::>(msg); if resp.is_err() { error!("{} is not a JSON string, {}", msg, EXCHANGE_NAME); return MiscMessage::Other; } let obj = resp.unwrap(); if let Some(event) = obj.get("event") { match event.as_str().unwrap() { "error" => error!("Received {} from {}", msg, EXCHANGE_NAME), "subscribe" => info!("Received {} from {}", msg, EXCHANGE_NAME), "unsubscribe" => info!("Received {} from {}", msg, EXCHANGE_NAME), _ => warn!("Received {} from {}", msg, EXCHANGE_NAME), } MiscMessage::Other } else if !obj.contains_key("arg") || !obj.contains_key("data") { error!("Received {} from {}", msg, EXCHANGE_NAME); MiscMessage::Other } else { MiscMessage::Normal } } fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)> { // https://bitgetlimited.github.io/apidoc/en/spot/#connect Some((Message::Text("ping".to_string()), 30)) } } impl CommandTranslator for BitgetCommandTranslator { fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec { ensure_frame_size(topics, subscribe, Self::topics_to_command, WS_FRAME_SIZE, None) } fn translate_to_candlestick_commands( &self, subscribe: bool, symbol_interval_list: &[(String, usize)], ) -> Vec { let topics = symbol_interval_list .iter() .map(|(symbol, interval)| { let channel = Self::to_candlestick_raw_channel(*interval); (channel.to_string(), symbol.to_string()) }) .collect::>(); self.translate_to_commands(subscribe, &topics) } } #[cfg(test)] mod tests { use crate::common::command_translator::CommandTranslator; #[test] fn test_one_topic() { let translator = super::BitgetCommandTranslator::<'S'> {}; let commands = translator.translate_to_commands(true, &[("trade".to_string(), "BTCUSDT".to_string())]); assert_eq!(1, commands.len()); assert_eq!( r#"{"op":"subscribe","args":[{"channel":"trade","instId":"BTCUSDT","instType":"SP"}]}"#, commands[0] ); } #[test] fn test_two_topics() { let translator = super::BitgetCommandTranslator::<'S'> {}; let commands = translator.translate_to_commands( true, &[ ("trade".to_string(), "BTCUSDT".to_string()), ("books".to_string(), "ETHUSDT".to_string()), ], ); assert_eq!(1, commands.len()); assert_eq!( r#"{"op":"subscribe","args":[{"channel":"trade","instId":"BTCUSDT","instType":"SP"},{"channel":"books","instId":"ETHUSDT","instType":"SP"}]}"#, commands[0] ); } #[test] fn test_candlestick() { let translator = super::BitgetCommandTranslator::<'S'> {}; let commands = translator.translate_to_candlestick_commands( true, &[("BTCUSDT".to_string(), 60), ("ETHUSDT".to_string(), 300)], ); assert_eq!(1, commands.len()); assert_eq!( r#"{"op":"subscribe","args":[{"channel":"candle1m","instId":"BTCUSDT","instType":"SP"},{"channel":"candle5m","instId":"ETHUSDT","instType":"SP"}]}"#, commands[0] ); } } ================================================ FILE: crypto-ws-client/src/clients/bithumb.rs ================================================ use async_trait::async_trait; use std::collections::HashMap; use tokio_tungstenite::tungstenite::Message; use crate::{ clients::common_traits::{ Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO, }, common::{ command_translator::CommandTranslator, message_handler::{MessageHandler, MiscMessage}, ws_client_internal::WSClientInternal, }, WSClient, }; use log::*; use serde_json::Value; pub(super) const EXCHANGE_NAME: &str = "bithumb"; const WEBSOCKET_URL: &str = "wss://global-api.bithumb.pro/message/realtime"; /// The WebSocket client for Bithumb. /// /// Bithumb has only Spot market. /// /// * WebSocket API doc: /// * Trading at: pub struct BithumbWSClient { client: WSClientInternal, translator: BithumbCommandTranslator, } impl_new_constructor!( BithumbWSClient, EXCHANGE_NAME, WEBSOCKET_URL, BithumbMessageHandler {}, BithumbCommandTranslator {} ); #[rustfmt::skip] impl_trait!(Trade, BithumbWSClient, subscribe_trade, "TRADE"); #[rustfmt::skip] impl_trait!(Ticker, BithumbWSClient, subscribe_ticker, "TICKER"); #[rustfmt::skip] impl_trait!(OrderBook, BithumbWSClient, subscribe_orderbook, "ORDERBOOK"); panic_bbo!(BithumbWSClient); panic_candlestick!(BithumbWSClient); panic_l2_topk!(BithumbWSClient); panic_l3_orderbook!(BithumbWSClient); impl_ws_client_trait!(BithumbWSClient); struct BithumbMessageHandler {} struct BithumbCommandTranslator {} impl MessageHandler for BithumbMessageHandler { fn handle_message(&mut self, msg: &str) -> MiscMessage { let obj = serde_json::from_str::>(msg).unwrap(); let code = obj.get("code").unwrap().as_str().unwrap(); let code = code.parse::().unwrap(); if code < 10000 { match code { 0 => MiscMessage::Pong, 6 => { let arr = obj.get("data").unwrap().as_array(); if arr.is_some() && arr.unwrap().is_empty() { // ignore empty data MiscMessage::Other } else { MiscMessage::Normal } } 7 => MiscMessage::Normal, _ => { debug!("Received {} from {}", msg, EXCHANGE_NAME); MiscMessage::Other } } } else { panic!("Received {msg} from {EXCHANGE_NAME}"); } } fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)> { Some((Message::Text(r#"{"cmd":"ping"}"#.to_string()), 60)) } } impl BithumbCommandTranslator { fn topics_to_command(topics: &[(String, String)], subscribe: bool) -> String { let raw_channels: Vec = topics.iter().map(|(channel, symbol)| format!("{channel}:{symbol}")).collect(); format!( r#"{{"cmd":"{}","args":{}}}"#, if subscribe { "subscribe" } else { "unsubscribe" }, serde_json::to_string(&raw_channels).unwrap() ) } } impl CommandTranslator for BithumbCommandTranslator { fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec { vec![Self::topics_to_command(topics, subscribe)] } fn translate_to_candlestick_commands( &self, _subscribe: bool, _symbol_interval_list: &[(String, usize)], ) -> Vec { panic!("Bithumb does NOT have candlestick channel"); } } #[cfg(test)] mod tests { use crate::common::command_translator::CommandTranslator; #[test] fn test_two_symbols() { let translator = super::BithumbCommandTranslator {}; let commands = translator.translate_to_commands( true, &[ ("TRADE".to_string(), "BTC-USDT".to_string()), ("TRADE".to_string(), "ETH-USDT".to_string()), ], ); assert_eq!(1, commands.len()); assert_eq!( r#"{"cmd":"subscribe","args":["TRADE:BTC-USDT","TRADE:ETH-USDT"]}"#, commands[0] ); } #[test] fn test_two_channels() { let translator = super::BithumbCommandTranslator {}; let commands = translator.translate_to_commands( true, &[ ("TRADE".to_string(), "BTC-USDT".to_string()), ("ORDERBOOK".to_string(), "BTC-USDT".to_string()), ], ); assert_eq!(1, commands.len()); assert_eq!( r#"{"cmd":"subscribe","args":["TRADE:BTC-USDT","ORDERBOOK:BTC-USDT"]}"#, commands[0] ); } } ================================================ FILE: crypto-ws-client/src/clients/bitmex.rs ================================================ use async_trait::async_trait; use std::{collections::HashMap, time::Duration}; use tokio_tungstenite::tungstenite::Message; use crate::{ clients::common_traits::{ Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO, }, common::{ command_translator::CommandTranslator, message_handler::{MessageHandler, MiscMessage}, ws_client_internal::WSClientInternal, }, WSClient, }; use log::*; use serde_json::Value; pub(super) const EXCHANGE_NAME: &str = "bitmex"; const WEBSOCKET_URL: &str = "wss://www.bitmex.com/realtime"; // Too many args sent. Max length is 20 const MAX_CHANNELS_PER_COMMAND: usize = 20; /// The WebSocket client for BitMEX. /// /// BitMEX has Swap and Future markets. /// /// * WebSocket API doc: /// * Trading at: pub struct BitmexWSClient { client: WSClientInternal, translator: BitmexCommandTranslator, } impl_new_constructor!( BitmexWSClient, EXCHANGE_NAME, WEBSOCKET_URL, BitmexMessageHandler {}, BitmexCommandTranslator {} ); impl_trait!(Trade, BitmexWSClient, subscribe_trade, "trade"); impl_trait!(BBO, BitmexWSClient, subscribe_bbo, "quote"); #[rustfmt::skip] impl_trait!(OrderBook, BitmexWSClient, subscribe_orderbook, "orderBookL2"); #[rustfmt::skip] impl_trait!(OrderBookTopK, BitmexWSClient, subscribe_orderbook_topk, "orderBook10"); impl_candlestick!(BitmexWSClient); panic_l3_orderbook!(BitmexWSClient); panic_ticker!(BitmexWSClient); impl_ws_client_trait!(BitmexWSClient); struct BitmexMessageHandler {} struct BitmexCommandTranslator {} impl BitmexCommandTranslator { fn topics_to_command(topics: &[(String, String)], subscribe: bool) -> String { let raw_channels = topics .iter() .map(|(channel, symbol)| format!("{channel}:{symbol}")) .collect::>(); format!( r#"{{"op":"{}","args":{}}}"#, if subscribe { "subscribe" } else { "unsubscribe" }, serde_json::to_string(&raw_channels).unwrap() ) } // see https://www.okx.com/docs-v5/en/#websocket-api-public-channel-candlesticks-channel fn to_candlestick_raw_channel(interval: usize) -> String { let interval_str = match interval { 60 => "1m", 300 => "5m", 3600 => "1h", 86400 => "1d", _ => panic!("BitMEX has intervals 1m,5m,1h,1d"), }; format!("tradeBin{interval_str}") } } impl MessageHandler for BitmexMessageHandler { fn handle_message(&mut self, msg: &str) -> MiscMessage { if msg == "pong" { return MiscMessage::Pong; } let resp = serde_json::from_str::>(msg); if resp.is_err() { error!("{} is not a JSON string, {}", msg, EXCHANGE_NAME); return MiscMessage::Other; } let obj = resp.unwrap(); if obj.contains_key("error") { let error_msg = obj.get("error").unwrap().as_str().unwrap(); let code = obj.get("status").unwrap().as_i64().unwrap(); match code { // Rate limit exceeded 429 => { error!("Received {} from {}", msg, EXCHANGE_NAME); std::thread::sleep(Duration::from_secs(3)); } 400 => { if error_msg.starts_with("Unknown") { panic!("Received {msg} from {EXCHANGE_NAME}"); } else if error_msg.starts_with("You are already subscribed to this topic") { info!("Received {} from {}", msg, EXCHANGE_NAME) } else { warn!("Received {} from {}", msg, EXCHANGE_NAME); } } _ => error!("Received {} from {}", msg, EXCHANGE_NAME), } MiscMessage::Other } else if obj.contains_key("success") || obj.contains_key("info") { info!("Received {} from {}", msg, EXCHANGE_NAME); MiscMessage::Other } else if obj.contains_key("table") && obj.contains_key("action") && obj.contains_key("data") { MiscMessage::Normal } else { warn!("Received {} from {}", msg, EXCHANGE_NAME); MiscMessage::Other } } fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)> { Some((Message::Text("ping".to_string()), 5)) } } impl CommandTranslator for BitmexCommandTranslator { fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec { let mut commands: Vec = Vec::new(); let n = topics.len(); for i in (0..n).step_by(MAX_CHANNELS_PER_COMMAND) { let chunk: Vec<(String, String)> = (topics[i..(std::cmp::min(i + MAX_CHANNELS_PER_COMMAND, n))]).to_vec(); commands.push(Self::topics_to_command(&chunk, subscribe)); } commands } fn translate_to_candlestick_commands( &self, subscribe: bool, symbol_interval_list: &[(String, usize)], ) -> Vec { let topics = symbol_interval_list .iter() .map(|(symbol, interval)| { let channel = Self::to_candlestick_raw_channel(*interval); (channel, symbol.to_string()) }) .collect::>(); self.translate_to_commands(subscribe, &topics) } } #[cfg(test)] mod tests { use crate::common::command_translator::CommandTranslator; #[test] fn test_one_topic() { let translator = super::BitmexCommandTranslator {}; let commands = translator.translate_to_commands(true, &[("trade".to_string(), "XBTUSD".to_string())]); assert_eq!(1, commands.len()); assert_eq!(r#"{"op":"subscribe","args":["trade:XBTUSD"]}"#, commands[0]); } #[test] fn test_multiple_topics() { let translator = super::BitmexCommandTranslator {}; let commands = translator.translate_to_commands( true, &[ ("trade".to_string(), "XBTUSD".to_string()), ("quote".to_string(), "XBTUSD".to_string()), ("orderBookL2_25".to_string(), "XBTUSD".to_string()), ("tradeBin1m".to_string(), "XBTUSD".to_string()), ], ); assert_eq!(1, commands.len()); assert_eq!( r#"{"op":"subscribe","args":["trade:XBTUSD","quote:XBTUSD","orderBookL2_25:XBTUSD","tradeBin1m:XBTUSD"]}"#, commands[0] ); } } ================================================ FILE: crypto-ws-client/src/clients/bitstamp.rs ================================================ use async_trait::async_trait; use std::collections::HashMap; use tokio_tungstenite::tungstenite::Message; use crate::{ clients::common_traits::{ Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO, }, common::{ command_translator::CommandTranslator, message_handler::{MessageHandler, MiscMessage}, ws_client_internal::WSClientInternal, }, WSClient, }; use log::*; use serde_json::Value; pub(super) const EXCHANGE_NAME: &str = "bitstamp"; const WEBSOCKET_URL: &str = "wss://ws.bitstamp.net"; /// The WebSocket client for Bitstamp Spot market. /// /// Bitstamp has only Spot market. /// /// * WebSocket API doc: /// * Trading at: pub struct BitstampWSClient { client: WSClientInternal, translator: BitstampCommandTranslator, } impl_new_constructor!( BitstampWSClient, EXCHANGE_NAME, WEBSOCKET_URL, BitstampMessageHandler {}, BitstampCommandTranslator {} ); impl_trait!(Trade, BitstampWSClient, subscribe_trade, "live_trades"); #[rustfmt::skip] impl_trait!(OrderBook, BitstampWSClient, subscribe_orderbook, "diff_order_book"); #[rustfmt::skip] impl_trait!(OrderBookTopK, BitstampWSClient, subscribe_orderbook_topk, "order_book"); #[rustfmt::skip] impl_trait!(Level3OrderBook, BitstampWSClient, subscribe_l3_orderbook, "live_orders"); panic_bbo!(BitstampWSClient); impl_candlestick!(BitstampWSClient); panic_ticker!(BitstampWSClient); impl_ws_client_trait!(BitstampWSClient); struct BitstampMessageHandler {} struct BitstampCommandTranslator {} impl MessageHandler for BitstampMessageHandler { fn handle_message(&mut self, msg: &str) -> MiscMessage { let resp = serde_json::from_str::>(msg); if resp.is_err() { error!("{} is not a JSON string, {}", msg, EXCHANGE_NAME); return MiscMessage::Other; } let obj = resp.unwrap(); let event = obj.get("event").unwrap().as_str().unwrap(); match event { "bts:subscription_succeeded" | "bts:unsubscription_succeeded" | "bts:heartbeat" => { debug!("Received {} from {}", msg, EXCHANGE_NAME); MiscMessage::Other } "bts:error" => { error!("Received {} from {}", msg, EXCHANGE_NAME); panic!("Received {msg} from {EXCHANGE_NAME}"); } "bts:request_reconnect" => { warn!("Received {}, which means Bitstamp is under maintenance", msg); std::thread::sleep(std::time::Duration::from_secs(20)); MiscMessage::Reconnect } _ => MiscMessage::Normal, } } fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)> { // See "Heartbeat" at https://www.bitstamp.net/websocket/v2/ Some((Message::Text(r#"{"event": "bts:heartbeat"}"#.to_string()), 10)) } } impl CommandTranslator for BitstampCommandTranslator { fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec { topics .iter() .map(|(channel, symbol)| { format!( r#"{{"event":"bts:{}","data":{{"channel":"{}_{}"}}}}"#, if subscribe { "subscribe" } else { "unsubscribe" }, channel, symbol, ) }) .collect() } fn translate_to_candlestick_commands( &self, _subscribe: bool, _symbol_interval_list: &[(String, usize)], ) -> Vec { panic!("Bitstamp does NOT have candlestick channel"); } } #[cfg(test)] mod tests { use crate::common::command_translator::CommandTranslator; #[test] fn test_one_topic() { let translator = super::BitstampCommandTranslator {}; let commands = translator .translate_to_commands(true, &[("live_trades".to_string(), "btcusd".to_string())]); assert_eq!(1, commands.len()); assert_eq!( r#"{"event":"bts:subscribe","data":{"channel":"live_trades_btcusd"}}"#, commands[0] ); } #[test] fn test_two_topics() { let translator = super::BitstampCommandTranslator {}; let commands = translator.translate_to_commands( true, &[ ("live_trades".to_string(), "btcusd".to_string()), ("diff_order_book".to_string(), "btcusd".to_string()), ], ); assert_eq!(2, commands.len()); assert_eq!( r#"{"event":"bts:subscribe","data":{"channel":"live_trades_btcusd"}}"#, commands[0] ); assert_eq!( r#"{"event":"bts:subscribe","data":{"channel":"diff_order_book_btcusd"}}"#, commands[1] ); } } ================================================ FILE: crypto-ws-client/src/clients/bitz/bitz_spot.rs ================================================ use async_trait::async_trait; use std::{ collections::HashMap, time::{SystemTime, UNIX_EPOCH}, }; use tokio_tungstenite::tungstenite::Message; use crate::{ clients::common_traits::{ Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO, }, common::{ command_translator::CommandTranslator, message_handler::{MessageHandler, MiscMessage}, ws_client_internal::WSClientInternal, }, WSClient, }; use log::*; use serde_json::Value; use super::EXCHANGE_NAME; const WEBSOCKET_URL: &str = "wss://wsapi.bitz.plus/"; /// The WebSocket client for Bitz spot market. /// /// * WebSocket API doc: /// * Trading at pub struct BitzSpotWSClient { client: WSClientInternal, translator: BitzCommandTranslator, } impl_new_constructor!( BitzSpotWSClient, EXCHANGE_NAME, WEBSOCKET_URL, BitzMessageHandler {}, BitzCommandTranslator {} ); #[rustfmt::skip] impl_trait!(Trade, BitzSpotWSClient, subscribe_trade, "order"); #[rustfmt::skip] impl_trait!(OrderBook, BitzSpotWSClient, subscribe_orderbook, "depth"); #[rustfmt::skip] impl_trait!(Ticker, BitzSpotWSClient, subscribe_ticker, "market"); impl_candlestick!(BitzSpotWSClient); panic_bbo!(BitzSpotWSClient); panic_l2_topk!(BitzSpotWSClient); panic_l3_orderbook!(BitzSpotWSClient); impl_ws_client_trait!(BitzSpotWSClient); struct BitzMessageHandler {} struct BitzCommandTranslator {} impl MessageHandler for BitzMessageHandler { fn handle_message(&mut self, msg: &str) -> MiscMessage { if msg == "pong" { return MiscMessage::Pong; } let obj = serde_json::from_str::>(msg).unwrap(); if obj.contains_key("action") && obj.get("action").unwrap().as_str().unwrap().starts_with("Pushdata.") { MiscMessage::Normal } else if obj.contains_key("status") { let status = obj.get("status").unwrap().as_i64().unwrap(); // see https://apidocv2.bitz.plus/en/#error match status { -101001 => { error!("Subscription type parameter error: {}", msg); panic!("Subscription type parameter error: {msg}"); } -101002 => { error!("Fail to get subscribed symbol of trading pair: {}", msg); panic!("Fail to get subscribed symbol of trading pair: {msg}"); } -101003 => { error!("k-line scale resolution error: {}", msg); panic!("k-line scale resolution error: {msg}"); } _ => warn!("Received {} from {}", msg, EXCHANGE_NAME), } MiscMessage::Other } else { warn!("Received {} from {}", msg, EXCHANGE_NAME); MiscMessage::Other } } fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)> { // See https://apidocv2.bitz.plus/en/#heartbeat-and-persistent-connection-strategy Some((Message::Text("ping".to_string()), 10)) } } impl BitzCommandTranslator { fn symbol_channels_to_command(pair: &str, channels: &[String], subscribe: bool) -> String { format!( r#"{{"action":"Topic.{}", "data":{{"symbol":"{}", "type":"{}", "_CDID":"100002", "dataType":"1"}}, "msg_id":{}}}"#, if subscribe { "sub" } else { "unsub" }, pair, channels.join(","), SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis(), ) } fn to_candlestick_command(symbol: &str, interval: usize, subscribe: bool) -> String { let interval_str = match interval { 60 => "1min", 300 => "5min", 900 => "15min", 1800 => "30min", 3600 => "60min", 14400 => "4hour", 86400 => "1day", 432000 => "5day", 604800 => "1week", 2592000 => "1mon", _ => panic!( "Bitz available intervals 1min,5min,15min,30min,60min,4hour,1day,5day,1week,1mon" ), }; format!( r#"{{"action":"Topic.{}", "data":{{"symbol":"{}", "type":"kline", "resolution":"{}", "_CDID":"100002", "dataType":"1"}}, "msg_id":{}}}"#, if subscribe { "sub" } else { "unsub" }, symbol, interval_str, SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis(), ) } } impl CommandTranslator for BitzCommandTranslator { fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec { let mut commands: Vec = Vec::new(); let mut symbol_channels = HashMap::>::new(); for (channel, symbol) in topics { match symbol_channels.get_mut(symbol) { Some(channels) => channels.push(channel.to_string()), None => { symbol_channels.insert(symbol.to_string(), vec![channel.to_string()]); } } } for (symbol, channels) in symbol_channels.iter() { commands.push(Self::symbol_channels_to_command(symbol, channels, subscribe)); } commands } fn translate_to_candlestick_commands( &self, subscribe: bool, symbol_interval_list: &[(String, usize)], ) -> Vec { symbol_interval_list .iter() .map(|(symbol, interval)| Self::to_candlestick_command(symbol, *interval, subscribe)) .collect::>() } } ================================================ FILE: crypto-ws-client/src/clients/bitz/mod.rs ================================================ mod bitz_spot; // mod bitz_swap; pub use bitz_spot::BitzSpotWSClient; // pub use bitz_swap::BitzSwapWSClient; pub(super) const EXCHANGE_NAME: &str = "bitz"; ================================================ FILE: crypto-ws-client/src/clients/bybit/bybit_inverse.rs ================================================ use async_trait::async_trait; use crate::{ clients::common_traits::{ Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO, }, common::{command_translator::CommandTranslator, ws_client_internal::WSClientInternal}, WSClient, }; use super::utils::{BybitMessageHandler, EXCHANGE_NAME}; const WEBSOCKET_URL: &str = "wss://stream.bybit.com/realtime"; /// Bybit Inverses markets. /// /// InverseFuture: /// * WebSocket API doc: /// * Trading at: /// /// InverseSwap: /// * WebSocket API doc: /// * Trading at: pub struct BybitInverseWSClient { client: WSClientInternal, translator: BybitInverseCommandTranslator, } impl_new_constructor!( BybitInverseWSClient, EXCHANGE_NAME, WEBSOCKET_URL, BybitMessageHandler {}, BybitInverseCommandTranslator {} ); impl_trait!(Trade, BybitInverseWSClient, subscribe_trade, "trade"); #[rustfmt::skip] // Prefer orderBookL2_25 over orderBook_200.100ms because /public/orderBook/L2 // returns a top 25 snapshot, which is the same depth as orderBookL2_25. impl_trait!(OrderBook, BybitInverseWSClient, subscribe_orderbook, "orderBookL2_25"); #[rustfmt::skip] impl_trait!(Ticker, BybitInverseWSClient, subscribe_ticker, "instrument_info.100ms"); impl_candlestick!(BybitInverseWSClient); panic_bbo!(BybitInverseWSClient); panic_l3_orderbook!(BybitInverseWSClient); panic_l2_topk!(BybitInverseWSClient); impl_ws_client_trait!(BybitInverseWSClient); struct BybitInverseCommandTranslator {} impl BybitInverseCommandTranslator { // https://bybit-exchange.github.io/docs/inverse_futures/#t-websocketklinev2 // https://bybit-exchange.github.io/docs/inverse/#t-websocketklinev2 fn to_candlestick_raw_channel(interval: usize) -> String { let interval_str = match interval { 60 => "1", 180 => "3", 300 => "5", 900 => "15", 1800 => "30", 3600 => "60", 7200 => "120", 14400 => "240", 21600 => "360", 86400 => "D", 604800 => "W", 2592000 => "M", _ => panic!( "Bybit InverseFuture has intervals 1min,5min,15min,30min,60min,4hour,1day,1week,1mon" ), }; format!("klineV2.{interval_str}") } } impl CommandTranslator for BybitInverseCommandTranslator { fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec { vec![super::utils::topics_to_command(topics, subscribe)] } fn translate_to_candlestick_commands( &self, subscribe: bool, symbol_interval_list: &[(String, usize)], ) -> Vec { let topics = symbol_interval_list .iter() .map(|(symbol, interval)| { let channel = Self::to_candlestick_raw_channel(*interval); (channel, symbol.to_string()) }) .collect::>(); self.translate_to_commands(subscribe, &topics) } } ================================================ FILE: crypto-ws-client/src/clients/bybit/bybit_linear_swap.rs ================================================ use async_trait::async_trait; use crate::{ clients::common_traits::{ Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO, }, common::{command_translator::CommandTranslator, ws_client_internal::WSClientInternal}, WSClient, }; use super::utils::{BybitMessageHandler, EXCHANGE_NAME}; const WEBSOCKET_URL: &str = "wss://stream.bybit.com/realtime_public"; /// Bybit LinearSwap market. /// /// * WebSocket API doc: /// * Trading at: pub struct BybitLinearSwapWSClient { client: WSClientInternal, translator: BybitLinearCommandTranslator, } impl_new_constructor!( BybitLinearSwapWSClient, EXCHANGE_NAME, WEBSOCKET_URL, BybitMessageHandler {}, BybitLinearCommandTranslator {} ); impl_trait!(Trade, BybitLinearSwapWSClient, subscribe_trade, "trade"); #[rustfmt::skip] // Prefer orderBookL2_25 over orderBook_200.100ms because /public/orderBook/L2 // returns a top 25 snapshot, which is the same depth as orderBookL2_25. impl_trait!(OrderBook, BybitLinearSwapWSClient, subscribe_orderbook, "orderBookL2_25"); #[rustfmt::skip] impl_trait!(Ticker, BybitLinearSwapWSClient, subscribe_ticker, "instrument_info.100ms"); impl_candlestick!(BybitLinearSwapWSClient); panic_bbo!(BybitLinearSwapWSClient); panic_l3_orderbook!(BybitLinearSwapWSClient); panic_l2_topk!(BybitLinearSwapWSClient); impl_ws_client_trait!(BybitLinearSwapWSClient); struct BybitLinearCommandTranslator {} impl BybitLinearCommandTranslator { // https://bybit-exchange.github.io/docs/linear/#t-websocketkline fn to_candlestick_raw_channel(interval: usize) -> String { let interval_str = match interval { 60 => "1", 180 => "3", 300 => "5", 900 => "15", 1800 => "30", 3600 => "60", 7200 => "120", 14400 => "240", 21600 => "360", 86400 => "D", 604800 => "W", 2592000 => "M", _ => panic!( "Bybit LinearSwap has intervals 1min,5min,15min,30min,60min,4hour,1day,1week,1mon" ), }; format!("candle.{interval_str}") } } impl CommandTranslator for BybitLinearCommandTranslator { fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec { vec![super::utils::topics_to_command(topics, subscribe)] } fn translate_to_candlestick_commands( &self, subscribe: bool, symbol_interval_list: &[(String, usize)], ) -> Vec { let topics = symbol_interval_list .iter() .map(|(symbol, interval)| { let channel = Self::to_candlestick_raw_channel(*interval); (channel, symbol.to_string()) }) .collect::>(); self.translate_to_commands(subscribe, &topics) } } ================================================ FILE: crypto-ws-client/src/clients/bybit/mod.rs ================================================ mod bybit_inverse; mod bybit_linear_swap; mod utils; pub use bybit_inverse::BybitInverseWSClient; pub use bybit_linear_swap::BybitLinearSwapWSClient; ================================================ FILE: crypto-ws-client/src/clients/bybit/utils.rs ================================================ use std::collections::HashMap; use log::*; use serde_json::Value; use tokio_tungstenite::tungstenite::Message; use crate::common::message_handler::{MessageHandler, MiscMessage}; pub(super) const EXCHANGE_NAME: &str = "bybit"; pub(super) fn topics_to_command(topics: &[(String, String)], subscribe: bool) -> String { let raw_channels = topics .iter() .map(|(channel, symbol)| format!("{channel}.{symbol}")) .collect::>(); format!( r#"{{"op":"{}","args":{}}}"#, if subscribe { "subscribe" } else { "unsubscribe" }, serde_json::to_string(&raw_channels).unwrap() ) } pub(super) struct BybitMessageHandler {} impl MessageHandler for BybitMessageHandler { fn handle_message(&mut self, msg: &str) -> MiscMessage { let obj = serde_json::from_str::>(msg).unwrap(); if obj.contains_key("topic") && obj.contains_key("data") { MiscMessage::Normal } else { if obj.contains_key("success") { if obj.get("success").unwrap().as_bool().unwrap() { info!("Received {} from {}", msg, EXCHANGE_NAME); if obj.contains_key("ret_msg") && obj.get("ret_msg").unwrap().as_str().unwrap() == "pong" { return MiscMessage::Pong; } } else { error!("Received {} from {}", msg, EXCHANGE_NAME); panic!("Received {msg} from {EXCHANGE_NAME}"); } } else { warn!("Received {} from {}", msg, EXCHANGE_NAME); } MiscMessage::Other } } fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)> { // See: // - https://bybit-exchange.github.io/docs/inverse/#t-heartbeat // - https://bybit-exchange.github.io/docs/linear/#t-heartbeat Some((Message::Text(r#"{"op":"ping"}"#.to_string()), 30)) } } #[cfg(test)] mod tests { #[test] fn test_one_channel() { let command = super::topics_to_command(&[("trade".to_string(), "BTCUSD".to_string())], true); assert_eq!(r#"{"op":"subscribe","args":["trade.BTCUSD"]}"#, command); } #[test] fn test_multiple_channels() { let command = super::topics_to_command( &[ ("trade".to_string(), "BTCUSD".to_string()), ("orderBookL2_25".to_string(), "BTCUSD".to_string()), ("instrument_info.100ms".to_string(), "BTCUSD".to_string()), ], true, ); assert_eq!( r#"{"op":"subscribe","args":["trade.BTCUSD","orderBookL2_25.BTCUSD","instrument_info.100ms.BTCUSD"]}"#, command ); } } ================================================ FILE: crypto-ws-client/src/clients/coinbase_pro.rs ================================================ use async_trait::async_trait; use std::collections::{BTreeMap, HashMap}; use tokio_tungstenite::tungstenite::Message; use crate::{ clients::common_traits::{ Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO, }, common::{ command_translator::CommandTranslator, message_handler::{MessageHandler, MiscMessage}, ws_client_internal::WSClientInternal, }, WSClient, }; use log::*; use serde_json::Value; pub(super) const EXCHANGE_NAME: &str = "coinbase_pro"; const WEBSOCKET_URL: &str = "wss://ws-feed.exchange.coinbase.com"; /// The WebSocket client for CoinbasePro. /// /// CoinbasePro has only Spot market. /// /// * WebSocket API doc: /// * Trading at: pub struct CoinbaseProWSClient { client: WSClientInternal, translator: CoinbaseProCommandTranslator, } impl_new_constructor!( CoinbaseProWSClient, EXCHANGE_NAME, WEBSOCKET_URL, CoinbaseProMessageHandler {}, CoinbaseProCommandTranslator {} ); impl_trait!(Trade, CoinbaseProWSClient, subscribe_trade, "matches"); impl_trait!(Ticker, CoinbaseProWSClient, subscribe_ticker, "ticker"); #[rustfmt::skip] impl_trait!(OrderBook, CoinbaseProWSClient, subscribe_orderbook, "level2"); #[rustfmt::skip] impl_trait!(Level3OrderBook, CoinbaseProWSClient, subscribe_l3_orderbook, "full"); panic_bbo!(CoinbaseProWSClient); panic_candlestick!(CoinbaseProWSClient); panic_l2_topk!(CoinbaseProWSClient); impl_ws_client_trait!(CoinbaseProWSClient); struct CoinbaseProMessageHandler {} struct CoinbaseProCommandTranslator {} impl MessageHandler for CoinbaseProMessageHandler { fn handle_message(&mut self, msg: &str) -> MiscMessage { let resp = serde_json::from_str::>(msg); if resp.is_err() { error!("{} is not a JSON string, {}", msg, EXCHANGE_NAME); return MiscMessage::Other; } let obj = resp.unwrap(); match obj.get("type").unwrap().as_str().unwrap() { "error" => { error!("Received {} from {}", msg, EXCHANGE_NAME); if obj.contains_key("reason") && obj .get("reason") .unwrap() .as_str() .unwrap() .contains("is not a valid product") { panic!("Received {msg} from {EXCHANGE_NAME}"); } else { MiscMessage::Other } } "subscriptions" => { info!("Received {} from {}", msg, EXCHANGE_NAME); MiscMessage::Other } "heartbeat" => { debug!("Received {} from {}", msg, EXCHANGE_NAME); MiscMessage::Other } _ => MiscMessage::Normal, } } fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)> { None } } impl CommandTranslator for CoinbaseProCommandTranslator { fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec { let mut commands: Vec = Vec::new(); let mut channel_symbols = BTreeMap::>::new(); for (channel, symbol) in topics { match channel_symbols.get_mut(channel) { Some(symbols) => symbols.push(symbol.to_string()), None => { channel_symbols.insert(channel.to_string(), vec![symbol.to_string()]); } } } if !channel_symbols.is_empty() { let mut command = String::new(); command.push_str( format!( r#"{{"type":"{}","channels": ["#, if subscribe { "subscribe" } else { "unsubscribe" } ) .as_str(), ); for (channel, symbols) in channel_symbols.iter() { command.push_str( format!( r#"{{"name":"{}","product_ids":{}}}"#, channel, serde_json::to_string(symbols).unwrap(), ) .as_str(), ); command.push(',') } command.pop(); command.push_str("]}"); commands.push(command); } commands } fn translate_to_candlestick_commands( &self, _subscribe: bool, _symbol_interval_list: &[(String, usize)], ) -> Vec { panic!("CoinbasePro does NOT have candlestick channel"); } } #[cfg(test)] mod tests { use crate::common::command_translator::CommandTranslator; #[test] fn test_two_symbols() { let translator = super::CoinbaseProCommandTranslator {}; let commands = translator.translate_to_commands( true, &[ ("matches".to_string(), "BTC-USD".to_string()), ("matches".to_string(), "ETH-USD".to_string()), ], ); assert_eq!(1, commands.len()); assert_eq!( r#"{"type":"subscribe","channels": [{"name":"matches","product_ids":["BTC-USD","ETH-USD"]}]}"#, commands[0] ); } #[test] fn test_two_channels() { let translator = super::CoinbaseProCommandTranslator {}; let commands = translator.translate_to_commands( true, &[ ("matches".to_string(), "BTC-USD".to_string()), ("level2".to_string(), "BTC-USD".to_string()), ], ); assert_eq!(1, commands.len()); assert_eq!( r#"{"type":"subscribe","channels": [{"name":"level2","product_ids":["BTC-USD"]},{"name":"matches","product_ids":["BTC-USD"]}]}"#, commands[0] ); } } ================================================ FILE: crypto-ws-client/src/clients/common_traits.rs ================================================ use async_trait::async_trait; // tick-by-tick trade #[async_trait] pub(super) trait Trade { async fn subscribe_trade(&self, symbols: &[String]); } // 24hr rolling window ticker #[async_trait] pub(super) trait Ticker { async fn subscribe_ticker(&self, symbols: &[String]); } // Best Bid & Offer #[allow(clippy::upper_case_acronyms)] #[async_trait] pub(super) trait BBO { async fn subscribe_bbo(&self, symbols: &[String]); } // An orderbook snapshot followed by realtime updates. #[async_trait] pub(super) trait OrderBook { async fn subscribe_orderbook(&self, symbols: &[String]); } #[async_trait] pub(super) trait OrderBookTopK { /// Subscribes to level2 orderbook top-k snapshot channels. async fn subscribe_orderbook_topk(&self, symbols: &[String]); } /// Level3 orderbook data. #[async_trait] pub(super) trait Level3OrderBook { /// Subscribes to level3 orderebook channels. /// /// The level3 orderbook is the orginal orderbook of an exchange, it is /// non-aggregated by price level and updated tick-by-tick. async fn subscribe_l3_orderbook(&self, symbols: &[String]); } #[async_trait] pub(super) trait Candlestick { /// Subscribes to candlestick channels which send OHLCV messages. /// /// `symbol_interval_list` is a list of symbols and intervals of /// candlesticks in seconds. async fn subscribe_candlestick(&self, symbol_interval_list: &[(String, usize)]); } macro_rules! impl_trait { ($trait_name:ident, $struct_name:ident, $method_name:ident, $channel:expr) => { #[async_trait] impl $trait_name for $struct_name { async fn $method_name(&self, symbols: &[String]) { let topics = symbols .iter() .map(|symbol| ($channel.to_string(), symbol.to_string())) .collect::>(); self.subscribe(&topics).await; } } }; } macro_rules! impl_candlestick { ($struct_name:ident) => { #[async_trait] impl Candlestick for $struct_name { async fn subscribe_candlestick(&self, symbol_interval_list: &[(String, usize)]) { let commands = self.translator.translate_to_candlestick_commands(true, symbol_interval_list); self.client.send(&commands).await; } } }; } macro_rules! panic_ticker { ($struct_name:ident) => { #[async_trait] impl Ticker for $struct_name { async fn subscribe_ticker(&self, _symbols: &[String]) { panic!("{} does NOT have the ticker websocket channel", EXCHANGE_NAME); } } }; } macro_rules! panic_bbo { ($struct_name:ident) => { #[async_trait] impl BBO for $struct_name { async fn subscribe_bbo(&self, _symbols: &[String]) { panic!("{} does NOT have the BBO websocket channel", EXCHANGE_NAME); } } }; } macro_rules! panic_l2 { ($struct_name:ident) => { #[async_trait] impl OrderBook for $struct_name { async fn subscribe_orderbook(&self, _symbols: &[String]) { panic!("{} does NOT have the incremental level2 websocket channel", EXCHANGE_NAME); } } }; } macro_rules! panic_l2_topk { ($struct_name:ident) => { #[async_trait] impl OrderBookTopK for $struct_name { async fn subscribe_orderbook_topk(&self, _symbols: &[String]) { panic!( "{} does NOT have the level2 top-k snapshot websocket channel", EXCHANGE_NAME ); } } }; } macro_rules! panic_l3_orderbook { ($struct_name:ident) => { #[async_trait] impl Level3OrderBook for $struct_name { async fn subscribe_l3_orderbook(&self, _symbols: &[String]) { panic!("{} does NOT have the level3 websocket channel", EXCHANGE_NAME); } } }; } macro_rules! panic_candlestick { ($struct_name:ident) => { #[async_trait] impl Candlestick for $struct_name { async fn subscribe_candlestick(&self, _symbol_interval_list: &[(String, usize)]) { panic!("{} does NOT have the candlestick websocket channel", EXCHANGE_NAME); } } }; } /// Implement the new() constructor. macro_rules! impl_new_constructor { ($struct_name:ident, $exchange:ident, $default_url:expr, $handler:expr, $translator:expr) => { impl $struct_name { /// Creates a websocket client. /// /// # Arguments /// /// * `tx` - The sending part of a channel /// * `url` - Optional server url, usually you don't need specify it pub async fn new(tx: std::sync::mpsc::Sender, url: Option<&str>) -> Self { let real_url = match url { Some(endpoint) => endpoint, None => $default_url, }; $struct_name { client: WSClientInternal::connect($exchange, real_url, $handler, None, tx) .await, translator: $translator, } } } }; } /// Implement the WSClient trait. macro_rules! impl_ws_client_trait { ($struct_name:ident) => { #[async_trait] impl WSClient for $struct_name { async fn subscribe_trade(&self, symbols: &[String]) { <$struct_name as Trade>::subscribe_trade(self, symbols).await } async fn subscribe_orderbook(&self, symbols: &[String]) { <$struct_name as OrderBook>::subscribe_orderbook(self, symbols).await } async fn subscribe_orderbook_topk(&self, symbols: &[String]) { <$struct_name as OrderBookTopK>::subscribe_orderbook_topk(self, symbols).await } async fn subscribe_l3_orderbook(&self, symbols: &[String]) { <$struct_name as Level3OrderBook>::subscribe_l3_orderbook(self, symbols).await } async fn subscribe_ticker(&self, symbols: &[String]) { <$struct_name as Ticker>::subscribe_ticker(self, symbols).await } async fn subscribe_bbo(&self, symbols: &[String]) { <$struct_name as BBO>::subscribe_bbo(self, symbols).await } async fn subscribe_candlestick(&self, symbol_interval_list: &[(String, usize)]) { <$struct_name as Candlestick>::subscribe_candlestick(self, symbol_interval_list) .await } async fn subscribe(&self, topics: &[(String, String)]) { let commands = self.translator.translate_to_commands(true, topics); self.client.send(&commands).await; } async fn unsubscribe(&self, topics: &[(String, String)]) { let commands = self.translator.translate_to_commands(false, topics); self.client.send(&commands).await; } async fn send(&self, commands: &[String]) { self.client.send(commands).await; } async fn run(&self) { self.client.run().await; } async fn close(&self) { self.client.close().await; } } }; } ================================================ FILE: crypto-ws-client/src/clients/deribit.rs ================================================ use async_trait::async_trait; use std::collections::HashMap; use tokio_tungstenite::tungstenite::Message; use crate::{ clients::common_traits::{ Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO, }, common::{ command_translator::CommandTranslator, message_handler::{MessageHandler, MiscMessage}, utils::{ensure_frame_size, topic_to_raw_channel}, ws_client_internal::WSClientInternal, }, WSClient, }; use log::*; use serde_json::Value; pub(super) const EXCHANGE_NAME: &str = "deribit"; const WEBSOCKET_URL: &str = "wss://www.deribit.com/ws/api/v2/"; // -32600 "request entity too large" /// single frame in websocket connection frame exceeds the limit (32 kB) const WS_FRAME_SIZE: usize = 32 * 1024; /// The WebSocket client for Deribit. /// /// Deribit has InverseFuture, InverseSwap and Option markets. /// /// * WebSocket API doc: /// * Trading at: /// * Future /// * Option pub struct DeribitWSClient { client: WSClientInternal, translator: DeribitCommandTranslator, } impl_new_constructor!( DeribitWSClient, EXCHANGE_NAME, WEBSOCKET_URL, DeribitMessageHandler {}, DeribitCommandTranslator {} ); #[rustfmt::skip] impl_trait!(Trade, DeribitWSClient, subscribe_trade, "trades.SYMBOL.100ms"); #[rustfmt::skip] impl_trait!(Ticker, DeribitWSClient, subscribe_ticker, "ticker.SYMBOL.100ms"); #[rustfmt::skip] impl_trait!(OrderBook, DeribitWSClient, subscribe_orderbook, "book.SYMBOL.100ms"); #[rustfmt::skip] impl_trait!(OrderBookTopK, DeribitWSClient, subscribe_orderbook_topk, "book.SYMBOL.none.20.100ms"); impl_trait!(BBO, DeribitWSClient, subscribe_bbo, "quote.SYMBOL"); impl_candlestick!(DeribitWSClient); panic_l3_orderbook!(DeribitWSClient); impl_ws_client_trait!(DeribitWSClient); struct DeribitMessageHandler {} struct DeribitCommandTranslator {} impl DeribitCommandTranslator { fn topics_to_command(topics: &[(String, String)], subscribe: bool) -> String { let raw_channels = topics.iter().map(topic_to_raw_channel).collect::>(); format!( r#"{{"method": "public/{}", "params": {{"channels": {}}}}}"#, if subscribe { "subscribe" } else { "unsubscribe" }, serde_json::to_string(&raw_channels).unwrap() ) } fn to_candlestick_channel(interval: usize) -> String { let interval_str = match interval { 60 => "1", 180 => "3", 300 => "5", 600 => "10", 900 => "15", 1800 => "30", 3600 => "60", 7200 => "120", 10800 => "180", 21600 => "360", 43200 => "720", 86400 => "1D", _ => panic!("Unknown interval {interval}"), }; format!("chart.trades.SYMBOL.{interval_str}") } } impl MessageHandler for DeribitMessageHandler { fn handle_message(&mut self, msg: &str) -> MiscMessage { let obj = serde_json::from_str::>(msg).unwrap(); if obj.contains_key("error") { panic!("Received {msg} from {EXCHANGE_NAME}"); } else if obj.contains_key("result") { info!("Received {} from {}", msg, EXCHANGE_NAME); MiscMessage::Other } else if obj.contains_key("method") && obj.contains_key("params") { match obj.get("method").unwrap().as_str().unwrap() { "subscription" => MiscMessage::Normal, "heartbeat" => { let param_type = obj .get("params") .unwrap() .as_object() .unwrap() .get("type") .unwrap() .as_str() .unwrap(); if param_type == "test_request" { let ws_msg = Message::Text(r#"{"method": "public/test"}"#.to_string()); MiscMessage::WebSocket(ws_msg) } else { info!("Received {} from {}", msg, EXCHANGE_NAME); MiscMessage::Other } } _ => { warn!("Received {} from {}", msg, EXCHANGE_NAME); MiscMessage::Other } } } else { error!("Received {} from {}", msg, EXCHANGE_NAME); MiscMessage::Other } } fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)> { None } } impl CommandTranslator for DeribitCommandTranslator { fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec { let mut all_commands: Vec = ensure_frame_size(topics, subscribe, Self::topics_to_command, WS_FRAME_SIZE, None); all_commands .push(r#"{"method": "public/set_heartbeat", "params": {"interval": 10}}"#.to_string()); all_commands } fn translate_to_candlestick_commands( &self, subscribe: bool, symbol_interval_list: &[(String, usize)], ) -> Vec { let topics = symbol_interval_list .iter() .map(|(symbol, interval)| (Self::to_candlestick_channel(*interval), symbol.clone())) .collect::>(); self.translate_to_commands(subscribe, &topics) } } #[cfg(test)] mod tests { use crate::common::command_translator::CommandTranslator; #[test] fn test_one_channel() { let translator = super::DeribitCommandTranslator {}; let commands = translator.translate_to_commands( true, &[("trades.SYMBOL.100ms".to_string(), "BTC-26MAR21".to_string())], ); assert_eq!(2, commands.len()); assert_eq!( r#"{"method": "public/subscribe", "params": {"channels": ["trades.BTC-26MAR21.100ms"]}}"#, commands[0] ); assert_eq!( r#"{"method": "public/set_heartbeat", "params": {"interval": 10}}"#, commands[1] ); } #[test] fn test_two_channel() { let translator = super::DeribitCommandTranslator {}; let commands = translator.translate_to_commands( true, &[ ("trades.SYMBOL.100ms".to_string(), "BTC-26MAR21".to_string()), ("ticker.SYMBOL.100ms".to_string(), "BTC-26MAR21".to_string()), ], ); assert_eq!(2, commands.len()); assert_eq!( r#"{"method": "public/subscribe", "params": {"channels": ["trades.BTC-26MAR21.100ms","ticker.BTC-26MAR21.100ms"]}}"#, commands[0] ); assert_eq!( r#"{"method": "public/set_heartbeat", "params": {"interval": 10}}"#, commands[1] ); } } ================================================ FILE: crypto-ws-client/src/clients/dydx/dydx_swap.rs ================================================ use async_trait::async_trait; use std::collections::HashMap; use tokio_tungstenite::tungstenite::Message; use crate::{ clients::common_traits::{ Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO, }, common::{ command_translator::CommandTranslator, message_handler::{MessageHandler, MiscMessage}, ws_client_internal::WSClientInternal, }, WSClient, }; use super::EXCHANGE_NAME; use log::*; use serde_json::Value; const WEBSOCKET_URL: &str = "wss://api.dydx.exchange/v3/ws"; /// The WebSocket client for dYdX perpetual markets. /// /// * WebSocket API doc: /// * Trading at: pub struct DydxSwapWSClient { client: WSClientInternal, translator: DydxCommandTranslator, } impl_new_constructor!( DydxSwapWSClient, EXCHANGE_NAME, WEBSOCKET_URL, DydxMessageHandler {}, DydxCommandTranslator {} ); impl_trait!(Trade, DydxSwapWSClient, subscribe_trade, "v3_trades"); #[rustfmt::skip] impl_trait!(OrderBook, DydxSwapWSClient, subscribe_orderbook, "v3_orderbook"); panic_ticker!(DydxSwapWSClient); panic_bbo!(DydxSwapWSClient); panic_l2_topk!(DydxSwapWSClient); panic_l3_orderbook!(DydxSwapWSClient); panic_candlestick!(DydxSwapWSClient); impl_ws_client_trait!(DydxSwapWSClient); struct DydxMessageHandler {} struct DydxCommandTranslator {} impl MessageHandler for DydxMessageHandler { fn handle_message(&mut self, msg: &str) -> MiscMessage { let obj = serde_json::from_str::>(msg).unwrap(); match obj.get("type").unwrap().as_str().unwrap() { "error" => { error!("Received {} from {}", msg, EXCHANGE_NAME); if obj.contains_key("message") && obj .get("message") .unwrap() .as_str() .unwrap() .starts_with("Invalid subscription id for channel") { panic!("Received {msg} from {EXCHANGE_NAME}"); } else { MiscMessage::Other } } "connected" | "pong" => { debug!("Received {} from {}", msg, EXCHANGE_NAME); MiscMessage::Other } "channel_data" | "subscribed" => MiscMessage::Normal, _ => { warn!("Received {} from {}", msg, EXCHANGE_NAME); MiscMessage::Other } } } fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)> { // https://docs.dydx.exchange/#v3-websocket-api // The server will send pings every 30s and expects a pong within 10s. // The server does not expect pings, but will respond with a pong if sent one. Some((Message::Text(r#"{"type":"ping"}"#.to_string()), 30)) } } impl DydxCommandTranslator { fn topic_to_command(topic: &(String, String), subscribe: bool) -> String { format!( r#"{{"type": "{}", "channel": "{}", "id": "{}"}}"#, if subscribe { "subscribe" } else { "unsubscribe" }, topic.0, topic.1, ) } } impl CommandTranslator for DydxCommandTranslator { fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec { topics.iter().map(|t| Self::topic_to_command(t, subscribe)).collect() } fn translate_to_candlestick_commands( &self, _subscribe: bool, _symbol_interval_list: &[(String, usize)], ) -> Vec { panic!("dYdX does NOT have candlestick channel"); } } #[cfg(test)] mod tests { use crate::common::command_translator::CommandTranslator; #[test] fn test_one_topic() { let translator = super::DydxCommandTranslator {}; let commands = translator .translate_to_commands(true, &[("v3_trades".to_string(), "BTC-USD".to_string())]); assert_eq!(1, commands.len()); assert_eq!( r#"{"type": "subscribe", "channel": "v3_trades", "id": "BTC-USD"}"#, commands[0] ); } #[test] fn test_two_topic() { let translator = super::DydxCommandTranslator {}; let commands = translator.translate_to_commands( true, &[ ("v3_trades".to_string(), "BTC-USD".to_string()), ("v3_orderbook".to_string(), "BTC-USD".to_string()), ], ); assert_eq!(2, commands.len()); assert_eq!( r#"{"type": "subscribe", "channel": "v3_trades", "id": "BTC-USD"}"#, commands[0] ); assert_eq!( r#"{"type": "subscribe", "channel": "v3_orderbook", "id": "BTC-USD"}"#, commands[1] ); } } ================================================ FILE: crypto-ws-client/src/clients/dydx/mod.rs ================================================ mod dydx_swap; pub use dydx_swap::DydxSwapWSClient; const EXCHANGE_NAME: &str = "dydx"; ================================================ FILE: crypto-ws-client/src/clients/ftx.rs ================================================ use async_trait::async_trait; use std::collections::HashMap; use tokio_tungstenite::tungstenite::Message; use crate::{ clients::common_traits::{ Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO, }, common::{ command_translator::CommandTranslator, message_handler::{MessageHandler, MiscMessage}, ws_client_internal::WSClientInternal, }, WSClient, }; use log::*; use serde_json::Value; pub(super) const EXCHANGE_NAME: &str = "ftx"; const WEBSOCKET_URL: &str = "wss://ftx.com/ws/"; /// The WebSocket client for FTX. /// /// FTX has Spot, LinearFuture, LinearSwap, Option, Move and BVOL markets. /// /// * WebSocket API doc: /// * Trading at pub struct FtxWSClient { client: WSClientInternal, translator: FtxCommandTranslator, } impl_new_constructor!( FtxWSClient, EXCHANGE_NAME, WEBSOCKET_URL, FtxMessageHandler {}, FtxCommandTranslator {} ); impl_trait!(Trade, FtxWSClient, subscribe_trade, "trades"); impl_trait!(BBO, FtxWSClient, subscribe_bbo, "ticker"); #[rustfmt::skip] impl_trait!(OrderBook, FtxWSClient, subscribe_orderbook, "orderbook"); panic_candlestick!(FtxWSClient); panic_l2_topk!(FtxWSClient); panic_l3_orderbook!(FtxWSClient); panic_ticker!(FtxWSClient); impl_ws_client_trait!(FtxWSClient); struct FtxMessageHandler {} struct FtxCommandTranslator {} impl MessageHandler for FtxMessageHandler { fn handle_message(&mut self, msg: &str) -> MiscMessage { let obj = serde_json::from_str::>(msg).unwrap(); let msg_type = obj.get("type").unwrap().as_str().unwrap(); match msg_type { // see https://docs.ftx.com/#response-format "pong" => MiscMessage::Pong, "subscribed" | "unsubscribed" | "info" => { info!("Received {} from {}", msg, EXCHANGE_NAME); MiscMessage::Other } "partial" | "update" => MiscMessage::Normal, "error" => { let code = obj.get("code").unwrap().as_i64().unwrap(); match code { 400 => { // Already subscribed warn!("Received {} from {}", msg, EXCHANGE_NAME); } _ => panic!("Received {msg} from {EXCHANGE_NAME}"), } MiscMessage::Other } _ => { warn!("Received {} from {}", msg, EXCHANGE_NAME); MiscMessage::Other } } } fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)> { // Send pings at regular intervals (every 15 seconds): {'op': 'ping'}. // You will see an {'type': 'pong'} response. Some((Message::Text(r#"{"op":"ping"}"#.to_string()), 15)) } } impl CommandTranslator for FtxCommandTranslator { fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec { topics .iter() .map(|(channel, symbol)| { format!( r#"{{"op":"{}","channel":"{}","market":"{}"}}"#, if subscribe { "subscribe" } else { "unsubscribe" }, channel, symbol ) }) .collect() } fn translate_to_candlestick_commands( &self, _subscribe: bool, _symbol_interval_list: &[(String, usize)], ) -> Vec { panic!("FTX does NOT have candlestick channel"); } } #[cfg(test)] mod tests { use crate::common::command_translator::CommandTranslator; #[test] fn test_one_topic() { let translator = super::FtxCommandTranslator {}; let commands = translator .translate_to_commands(true, &[("trades".to_string(), "BTC/USD".to_string())]); assert_eq!(1, commands.len()); assert_eq!(r#"{"op":"subscribe","channel":"trades","market":"BTC/USD"}"#, commands[0]); } #[test] fn test_two_topic() { let translator = super::FtxCommandTranslator {}; let commands = translator.translate_to_commands( true, &[ ("trades".to_string(), "BTC/USD".to_string()), ("orderbook".to_string(), "BTC/USD".to_string()), ], ); assert_eq!(2, commands.len()); assert_eq!(r#"{"op":"subscribe","channel":"trades","market":"BTC/USD"}"#, commands[0]); assert_eq!(r#"{"op":"subscribe","channel":"orderbook","market":"BTC/USD"}"#, commands[1]); } } ================================================ FILE: crypto-ws-client/src/clients/gate/gate_future.rs ================================================ use async_trait::async_trait; use super::utils::{GateCommandTranslator, GateMessageHandler, EXCHANGE_NAME}; use crate::{ clients::common_traits::{ Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO, }, common::{command_translator::CommandTranslator, ws_client_internal::WSClientInternal}, WSClient, }; const INVERSE_FUTURE_WEBSOCKET_URL: &str = "wss://fx-ws.gateio.ws/v4/ws/delivery/btc"; const LINEAR_FUTURE_WEBSOCKET_URL: &str = "wss://fx-ws.gateio.ws/v4/ws/delivery/usdt"; /// The WebSocket client for Gate InverseFuture market. /// /// * WebSocket API doc: /// * Trading at pub struct GateInverseFutureWSClient { client: WSClientInternal>, translator: GateCommandTranslator<'F'>, } /// The WebSocket client for Gate LinearFuture market. /// /// * WebSocket API doc: /// * Trading at pub struct GateLinearFutureWSClient { client: WSClientInternal>, translator: GateCommandTranslator<'F'>, } impl_new_constructor!( GateInverseFutureWSClient, EXCHANGE_NAME, INVERSE_FUTURE_WEBSOCKET_URL, GateMessageHandler::<'F'> {}, GateCommandTranslator::<'F'> {} ); impl_new_constructor!( GateLinearFutureWSClient, EXCHANGE_NAME, LINEAR_FUTURE_WEBSOCKET_URL, GateMessageHandler::<'F'> {}, GateCommandTranslator::<'F'> {} ); impl_trait!(Trade, GateInverseFutureWSClient, subscribe_trade, "trades"); #[rustfmt::skip] impl_trait!(OrderBook, GateInverseFutureWSClient, subscribe_orderbook, "order_book"); #[rustfmt::skip] impl_trait!(Ticker, GateInverseFutureWSClient, subscribe_ticker, "tickers"); #[rustfmt::skip] impl_trait!(Trade, GateLinearFutureWSClient, subscribe_trade, "trades"); #[rustfmt::skip] impl_trait!(OrderBook, GateLinearFutureWSClient, subscribe_orderbook, "order_book"); #[rustfmt::skip] impl_trait!(Ticker, GateLinearFutureWSClient, subscribe_ticker, "tickers"); impl_candlestick!(GateInverseFutureWSClient); impl_candlestick!(GateLinearFutureWSClient); panic_bbo!(GateInverseFutureWSClient); panic_bbo!(GateLinearFutureWSClient); panic_l2_topk!(GateInverseFutureWSClient); panic_l2_topk!(GateLinearFutureWSClient); panic_l3_orderbook!(GateInverseFutureWSClient); panic_l3_orderbook!(GateLinearFutureWSClient); impl_ws_client_trait!(GateInverseFutureWSClient); impl_ws_client_trait!(GateLinearFutureWSClient); ================================================ FILE: crypto-ws-client/src/clients/gate/gate_spot.rs ================================================ use async_trait::async_trait; use super::utils::{GateCommandTranslator, GateMessageHandler, EXCHANGE_NAME}; use crate::{ clients::common_traits::{ Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO, }, common::{command_translator::CommandTranslator, ws_client_internal::WSClientInternal}, WSClient, }; const WEBSOCKET_URL: &str = "wss://api.gateio.ws/ws/v4/"; /// The WebSocket client for Gate spot market. /// /// * WebSocket API doc: /// * Trading at pub struct GateSpotWSClient { client: WSClientInternal>, translator: GateCommandTranslator<'S'>, } impl_new_constructor!( GateSpotWSClient, EXCHANGE_NAME, WEBSOCKET_URL, GateMessageHandler::<'S'> {}, GateCommandTranslator::<'S'> {} ); impl_trait!(Trade, GateSpotWSClient, subscribe_trade, "trades"); #[rustfmt::skip] impl_trait!(OrderBook, GateSpotWSClient, subscribe_orderbook, "order_book_update"); #[rustfmt::skip] impl_trait!(OrderBookTopK, GateSpotWSClient, subscribe_orderbook_topk, "order_book"); impl_trait!(BBO, GateSpotWSClient, subscribe_bbo, "book_ticker"); impl_trait!(Ticker, GateSpotWSClient, subscribe_ticker, "tickers"); impl_candlestick!(GateSpotWSClient); panic_l3_orderbook!(GateSpotWSClient); impl_ws_client_trait!(GateSpotWSClient); ================================================ FILE: crypto-ws-client/src/clients/gate/gate_swap.rs ================================================ use async_trait::async_trait; use super::utils::{GateCommandTranslator, GateMessageHandler, EXCHANGE_NAME}; use crate::{ clients::common_traits::{ Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO, }, common::{command_translator::CommandTranslator, ws_client_internal::WSClientInternal}, WSClient, }; const INVERSE_SWAP_WEBSOCKET_URL: &str = "wss://fx-ws.gateio.ws/v4/ws/btc"; const LINEAR_SWAP_WEBSOCKET_URL: &str = "wss://fx-ws.gateio.ws/v4/ws/usdt"; /// The WebSocket client for Gate InverseSwap market. /// /// * WebSocket API doc: /// * Trading at pub struct GateInverseSwapWSClient { client: WSClientInternal>, translator: GateCommandTranslator<'F'>, } /// The WebSocket client for Gate LinearSwap market. /// /// * WebSocket API doc: /// * Trading at pub struct GateLinearSwapWSClient { client: WSClientInternal>, translator: GateCommandTranslator<'F'>, } impl_new_constructor!( GateInverseSwapWSClient, EXCHANGE_NAME, INVERSE_SWAP_WEBSOCKET_URL, GateMessageHandler::<'F'> {}, GateCommandTranslator::<'F'> {} ); impl_new_constructor!( GateLinearSwapWSClient, EXCHANGE_NAME, LINEAR_SWAP_WEBSOCKET_URL, GateMessageHandler::<'F'> {}, GateCommandTranslator::<'F'> {} ); impl_trait!(Trade, GateInverseSwapWSClient, subscribe_trade, "trades"); #[rustfmt::skip] impl_trait!(OrderBook, GateInverseSwapWSClient, subscribe_orderbook, "order_book_update"); #[rustfmt::skip] impl_trait!(OrderBookTopK, GateInverseSwapWSClient, subscribe_orderbook_topk, "order_book"); impl_trait!(BBO, GateInverseSwapWSClient, subscribe_bbo, "book_ticker"); impl_trait!(Ticker, GateInverseSwapWSClient, subscribe_ticker, "tickers"); impl_trait!(Trade, GateLinearSwapWSClient, subscribe_trade, "trades"); #[rustfmt::skip] impl_trait!(OrderBook, GateLinearSwapWSClient, subscribe_orderbook, "order_book_update"); #[rustfmt::skip] impl_trait!(OrderBookTopK, GateLinearSwapWSClient, subscribe_orderbook_topk, "order_book"); impl_trait!(BBO, GateLinearSwapWSClient, subscribe_bbo, "book_ticker"); impl_trait!(Ticker, GateLinearSwapWSClient, subscribe_ticker, "tickers"); impl_candlestick!(GateInverseSwapWSClient); impl_candlestick!(GateLinearSwapWSClient); panic_l3_orderbook!(GateInverseSwapWSClient); panic_l3_orderbook!(GateLinearSwapWSClient); impl_ws_client_trait!(GateInverseSwapWSClient); impl_ws_client_trait!(GateLinearSwapWSClient); ================================================ FILE: crypto-ws-client/src/clients/gate/mod.rs ================================================ mod gate_future; mod gate_spot; mod gate_swap; mod utils; pub use gate_future::{GateInverseFutureWSClient, GateLinearFutureWSClient}; pub use gate_spot::GateSpotWSClient; pub use gate_swap::{GateInverseSwapWSClient, GateLinearSwapWSClient}; ================================================ FILE: crypto-ws-client/src/clients/gate/utils.rs ================================================ use std::collections::HashMap; use log::*; use serde_json::Value; use tokio_tungstenite::tungstenite::Message; use crate::common::{ command_translator::CommandTranslator, message_handler::{MessageHandler, MiscMessage}, }; pub(super) const EXCHANGE_NAME: &str = "gate"; // MARKET_TYPE: 'S' for spot, 'F' for futures pub(super) struct GateMessageHandler {} pub(super) struct GateCommandTranslator {} impl MessageHandler for GateMessageHandler { fn handle_message(&mut self, msg: &str) -> MiscMessage { let obj = serde_json::from_str::>(msg).unwrap(); // https://www.gate.io/docs/apiv4/ws/en/#server-response // Null if the server accepts the client request; otherwise, the detailed reason // why request is rejected. let error = match obj.get("error") { None => serde_json::Value::Null, Some(err) => { if err.is_null() { serde_json::Value::Null } else { err.clone() } } }; if !error.is_null() { let err = error.as_object().unwrap(); // https://www.gate.io/docs/apiv4/ws/en/#schema_error // https://www.gate.io/docs/futures/ws/en/#error let code = err.get("code").unwrap().as_i64().unwrap(); match code { 1 | 2 => panic!("Received {msg} from {EXCHANGE_NAME}"), // client side errors _ => error!("Received {} from {}", msg, EXCHANGE_NAME), // server side errors } return MiscMessage::Other; } let channel = obj.get("channel").unwrap().as_str().unwrap(); let event = obj.get("event").unwrap().as_str().unwrap(); if channel == "spot.pong" || channel == "futures.pong" { MiscMessage::Pong } else if event == "update" || event == "all" { MiscMessage::Normal } else if event == "subscribe" || event == "unsubscribe" { debug!("Received {} from {}", msg, EXCHANGE_NAME); MiscMessage::Other } else { warn!("Received {} from {}", msg, EXCHANGE_NAME); MiscMessage::Other } } fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)> { if MARKET_TYPE == 'S' { // https://www.gate.io/docs/apiv4/ws/en/#application-ping-pong Some((Message::Text(r#"{"channel":"spot.ping"}"#.to_string()), 60)) } else { // https://www.gate.io/docs/futures/ws/en/#ping-and-pong // https://www.gate.io/docs/delivery/ws/en/#ping-and-pong Some((Message::Text(r#"{"channel":"futures.ping"}"#.to_string()), 60)) } } } impl GateCommandTranslator { fn channel_symbols_to_command( channel: &str, symbols: &[String], subscribe: bool, ) -> Vec { let channel = if MARKET_TYPE == 'S' { format!("spot.{channel}") } else if MARKET_TYPE == 'F' { format!("futures.{channel}") } else { panic!("unexpected market type: {MARKET_TYPE}") }; if channel.contains(".order_book") { symbols .iter() .map(|symbol| { format!( r#"{{"channel":"{}", "event":"{}", "payload":{}}}"#, channel, if subscribe { "subscribe" } else { "unsubscribe" }, if channel.ends_with(".order_book") { if MARKET_TYPE == 'S' { serde_json::to_string(&[symbol, "20", "1000ms"]).unwrap() } else if MARKET_TYPE == 'F' { serde_json::to_string(&[symbol, "20", "0"]).unwrap() } else { panic!("unexpected market type: {MARKET_TYPE}") } } else if channel.ends_with(".order_book_update") { if MARKET_TYPE == 'S' { serde_json::to_string(&[symbol, "100ms"]).unwrap() } else if MARKET_TYPE == 'F' { serde_json::to_string(&[symbol, "100ms", "20"]).unwrap() } else { panic!("unexpected market type: {MARKET_TYPE}") } } else { panic!("unexpected channel: {channel}") }, ) }) .collect() } else { vec![format!( r#"{{"channel":"{}", "event":"{}", "payload":{}}}"#, channel, if subscribe { "subscribe" } else { "unsubscribe" }, serde_json::to_string(&symbols).unwrap(), )] } } fn to_candlestick_command(symbol: &str, interval: usize, subscribe: bool) -> String { let interval_str = match interval { 10 => "10s", 60 => "1m", 300 => "5m", 900 => "15m", 1800 => "30m", 3600 => "1h", 14400 => "4h", 28800 => "8h", 86400 => "1d", 604800 => "7d", _ => panic!("Gate available intervals 10s,1m,5m,15m,30m,1h,4h,8h,1d,7d"), }; format!( r#"{{"channel": "{}.candlesticks", "event": "{}", "payload" : ["{}", "{}"]}}"#, if MARKET_TYPE == 'S' { "spot" } else { "futures" }, if subscribe { "subscribe" } else { "unsubscribe" }, interval_str, symbol ) } } impl CommandTranslator for GateCommandTranslator { fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec { let mut commands: Vec = Vec::new(); let mut channel_symbols = HashMap::>::new(); for (channel, symbol) in topics { match channel_symbols.get_mut(channel) { Some(symbols) => symbols.push(symbol.to_string()), None => { channel_symbols.insert(channel.to_string(), vec![symbol.to_string()]); } } } for (channel, symbols) in channel_symbols.iter() { commands.extend(Self::channel_symbols_to_command(channel, symbols, subscribe)); } commands } fn translate_to_candlestick_commands( &self, subscribe: bool, symbol_interval_list: &[(String, usize)], ) -> Vec { symbol_interval_list .iter() .map(|(symbol, interval)| Self::to_candlestick_command(symbol, *interval, subscribe)) .collect::>() } } #[cfg(test)] mod tests { use crate::common::command_translator::CommandTranslator; #[test] fn test_spot() { let translator = super::GateCommandTranslator::<'S'> {}; assert_eq!( r#"{"channel":"spot.trades", "event":"subscribe", "payload":["BTC_USDT","ETH_USDT"]}"#, translator.translate_to_commands( true, &[ ("trades".to_string(), "BTC_USDT".to_string()), ("trades".to_string(), "ETH_USDT".to_string()) ] )[0] ); let commands = translator.translate_to_commands( true, &[ ("order_book".to_string(), "BTC_USDT".to_string()), ("order_book".to_string(), "ETH_USDT".to_string()), ], ); assert_eq!(2, commands.len()); assert_eq!( r#"{"channel":"spot.order_book", "event":"subscribe", "payload":["BTC_USDT","20","1000ms"]}"#, commands[0] ); assert_eq!( r#"{"channel":"spot.order_book", "event":"subscribe", "payload":["ETH_USDT","20","1000ms"]}"#, commands[1] ); let commands = translator.translate_to_commands( true, &[ ("order_book_update".to_string(), "BTC_USDT".to_string()), ("order_book_update".to_string(), "ETH_USDT".to_string()), ], ); assert_eq!(2, commands.len()); assert_eq!( r#"{"channel":"spot.order_book_update", "event":"subscribe", "payload":["BTC_USDT","100ms"]}"#, commands[0] ); assert_eq!( r#"{"channel":"spot.order_book_update", "event":"subscribe", "payload":["ETH_USDT","100ms"]}"#, commands[1] ); } #[test] fn test_futures() { let translator = super::GateCommandTranslator::<'F'> {}; assert_eq!( r#"{"channel":"futures.trades", "event":"subscribe", "payload":["BTC_USD","ETH_USD"]}"#, translator.translate_to_commands( true, &[ ("trades".to_string(), "BTC_USD".to_string()), ("trades".to_string(), "ETH_USD".to_string()) ] )[0] ); let commands = translator.translate_to_commands( true, &[ ("order_book".to_string(), "BTC_USD".to_string()), ("order_book".to_string(), "ETH_USD".to_string()), ], ); assert_eq!(2, commands.len()); assert_eq!( r#"{"channel":"futures.order_book", "event":"subscribe", "payload":["BTC_USD","20","0"]}"#, commands[0] ); assert_eq!( r#"{"channel":"futures.order_book", "event":"subscribe", "payload":["ETH_USD","20","0"]}"#, commands[1] ); let commands = translator.translate_to_commands( true, &[ ("order_book_update".to_string(), "BTC_USD".to_string()), ("order_book_update".to_string(), "ETH_USD".to_string()), ], ); assert_eq!(2, commands.len()); assert_eq!( r#"{"channel":"futures.order_book_update", "event":"subscribe", "payload":["BTC_USD","100ms","20"]}"#, commands[0] ); assert_eq!( r#"{"channel":"futures.order_book_update", "event":"subscribe", "payload":["ETH_USD","100ms","20"]}"#, commands[1] ); } } ================================================ FILE: crypto-ws-client/src/clients/huobi.rs ================================================ use async_trait::async_trait; use std::collections::HashMap; use log::*; use serde_json::Value; use tokio_tungstenite::tungstenite::Message; use crate::{ common::{ command_translator::CommandTranslator, message_handler::{MessageHandler, MiscMessage}, ws_client_internal::WSClientInternal, }, WSClient, }; pub(crate) const EXCHANGE_NAME: &str = "huobi"; // or wss://api-aws.huobi.pro/feed const SPOT_WEBSOCKET_URL: &str = "wss://api.huobi.pro/ws"; // const FUTURES_WEBSOCKET_URL: &str = "wss://www.hbdm.com/ws"; // const COIN_SWAP_WEBSOCKET_URL: &str = "wss://api.hbdm.com/swap-ws"; // const USDT_SWAP_WEBSOCKET_URL: &str = "wss://api.hbdm.com/linear-swap-ws"; // const OPTION_WEBSOCKET_URL: &str = "wss://api.hbdm.com/option-ws"; const FUTURES_WEBSOCKET_URL: &str = "wss://futures.huobi.com/ws"; const COIN_SWAP_WEBSOCKET_URL: &str = "wss://futures.huobi.com/swap-ws"; const USDT_SWAP_WEBSOCKET_URL: &str = "wss://futures.huobi.com/linear-swap-ws"; const OPTION_WEBSOCKET_URL: &str = "wss://futures.huobi.com/option-ws"; // Internal unified client pub struct HuobiWSClient { client: WSClientInternal, translator: HuobiCommandTranslator, } /// Huobi Spot market. /// /// * WebSocket API doc: /// * Trading at: pub type HuobiSpotWSClient = HuobiWSClient<'S'>; /// Huobi Future market. /// /// * WebSocket API doc: /// * Trading at: pub type HuobiFutureWSClient = HuobiWSClient<'F'>; /// Huobi Inverse Swap market. /// /// Inverse Swap market uses coins like BTC as collateral. /// /// * WebSocket API doc: /// * Trading at: pub type HuobiInverseSwapWSClient = HuobiWSClient<'I'>; /// Huobi Linear Swap market. /// /// Linear Swap market uses USDT as collateral. /// /// * WebSocket API doc: /// * Trading at: pub type HuobiLinearSwapWSClient = HuobiWSClient<'L'>; /// Huobi Option market. /// /// /// * WebSocket API doc: /// * Trading at: pub type HuobiOptionWSClient = HuobiWSClient<'O'>; impl HuobiWSClient { pub async fn new(tx: std::sync::mpsc::Sender, url: Option<&str>) -> Self { let real_url = match url { Some(endpoint) => endpoint, None => { if URL == 'S' { SPOT_WEBSOCKET_URL } else if URL == 'F' { FUTURES_WEBSOCKET_URL } else if URL == 'I' { COIN_SWAP_WEBSOCKET_URL } else if URL == 'L' { USDT_SWAP_WEBSOCKET_URL } else if URL == 'O' { OPTION_WEBSOCKET_URL } else { panic!("Unknown URL {URL}"); } } }; HuobiWSClient { client: WSClientInternal::connect( EXCHANGE_NAME, real_url, HuobiMessageHandler {}, None, tx, ) .await, translator: HuobiCommandTranslator {}, } } } #[async_trait] impl WSClient for HuobiWSClient { async fn subscribe_trade(&self, symbols: &[String]) { let topics = symbols .iter() .map(|symbol| ("trade.detail".to_string(), symbol.to_string())) .collect::>(); self.subscribe(&topics).await; } async fn subscribe_orderbook(&self, symbols: &[String]) { if URL == 'S' { let topics = symbols .iter() .map(|symbol| ("mbp.20".to_string(), symbol.to_string())) .collect::>(); self.subscribe(&topics).await; } else { let commands = symbols .iter() .map(|symbol| format!(r#"{{"sub":"market.{symbol}.depth.size_20.high_freq","data_type":"incremental","id": "crypto-ws-client"}}"#)) .collect::>(); self.client.send(&commands).await; } } async fn subscribe_orderbook_topk(&self, symbols: &[String]) { let channel = if URL == 'S' { "depth.step1" } else { "depth.step7" }; let topics = symbols .iter() .map(|symbol| (channel.to_string(), symbol.to_string())) .collect::>(); self.subscribe(&topics).await; } async fn subscribe_l3_orderbook(&self, _symbols: &[String]) { panic!("{EXCHANGE_NAME} does NOT have the level3 websocket channel"); } async fn subscribe_ticker(&self, symbols: &[String]) { let topics = symbols .iter() .map(|symbol| ("detail".to_string(), symbol.to_string())) .collect::>(); self.subscribe(&topics).await; } async fn subscribe_bbo(&self, symbols: &[String]) { let topics = symbols .iter() .map(|symbol| ("bbo".to_string(), symbol.to_string())) .collect::>(); self.subscribe(&topics).await; } async fn subscribe_candlestick(&self, symbol_interval_list: &[(String, usize)]) { let commands = self.translator.translate_to_candlestick_commands(true, symbol_interval_list); self.client.send(&commands).await; } async fn subscribe(&self, topics: &[(String, String)]) { let commands = self.translator.translate_to_commands(true, topics); self.client.send(&commands).await; } async fn unsubscribe(&self, topics: &[(String, String)]) { let commands = self.translator.translate_to_commands(false, topics); self.client.send(&commands).await; } async fn send(&self, commands: &[String]) { self.client.send(commands).await; } async fn run(&self) { self.client.run().await; } async fn close(&self) { self.client.close().await; } } struct HuobiMessageHandler {} struct HuobiCommandTranslator {} impl HuobiCommandTranslator { fn topic_to_command(channel: &str, symbol: &str, subscribe: bool) -> String { let raw_channel = format!("market.{symbol}.{channel}"); format!( r#"{{"{}":"{}","id":"crypto-ws-client"}}"#, if subscribe { "sub" } else { "unsub" }, raw_channel ) } // see https://huobiapi.github.io/docs/dm/v1/en/#subscribe-kline-data fn to_candlestick_raw_channel(interval: usize) -> String { let interval_str = match interval { 60 => "1min", 300 => "5min", 900 => "15min", 1800 => "30min", 3600 => "60min", 14400 => "4hour", 86400 => "1day", 604800 => "1week", 2592000 => "1mon", _ => panic!("Huobi has intervals 1min,5min,15min,30min,60min,4hour,1day,1week,1mon"), }; format!("kline.{interval_str}") } } impl MessageHandler for HuobiMessageHandler { fn handle_message(&mut self, msg: &str) -> MiscMessage { let resp = serde_json::from_str::>(msg); if resp.is_err() { error!("{} is not a JSON string, {}", msg, EXCHANGE_NAME); return MiscMessage::Other; } let obj = resp.unwrap(); // Market Heartbeat if obj.contains_key("ping") { // The server will send a heartbeat every 5 seconds // see links in get_ping_msg_and_interval()S debug!("Received {} from {}", msg, EXCHANGE_NAME); let timestamp = obj.get("ping").unwrap(); let mut pong_msg = HashMap::::new(); pong_msg.insert("pong".to_string(), timestamp); let ws_msg = Message::Text(serde_json::to_string(&pong_msg).unwrap()); return MiscMessage::WebSocket(ws_msg); } // Order Push Heartbeat // https://huobiapi.github.io/docs/usdt_swap/v1/en/#market-heartbeat if obj.contains_key("op") && obj.get("op").unwrap().as_str().unwrap() == "ping" { debug!("Received {} from {}", msg, EXCHANGE_NAME); let mut pong_msg = obj; pong_msg.insert("op".to_string(), serde_json::from_str("\"pong\"").unwrap()); // change ping to pong let ws_msg = Message::Text(serde_json::to_string(&pong_msg).unwrap()); return MiscMessage::WebSocket(ws_msg); } if (obj.contains_key("ch") || obj.contains_key("topic")) && obj.contains_key("ts") && (obj.contains_key("tick") || obj.contains_key("data")) { MiscMessage::Normal } else { if let Some(status) = obj.get("status") { match status.as_str().unwrap() { "ok" => info!("Received {} from {}", msg, EXCHANGE_NAME), "error" => { error!("Received {} from {}", msg, EXCHANGE_NAME); let err_msg = obj.get("err-msg").unwrap().as_str().unwrap(); if err_msg.starts_with("invalid") { panic!("Received {msg} from {EXCHANGE_NAME}"); } } _ => warn!("Received {} from {}", msg, EXCHANGE_NAME), } } else if let Some(op) = obj.get("op") { match op.as_str().unwrap() { "sub" | "unsub" => MiscMessage::Other, "notify" => MiscMessage::Normal, _ => { warn!("Received {} from {}", msg, EXCHANGE_NAME); MiscMessage::Other } }; } else { warn!("Received {} from {}", msg, EXCHANGE_NAME); } MiscMessage::Other } } fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)> { // The server will send a heartbeat every 5 seconds, // - Spot // - Future // - InverseSwap // - LinearSwap // - Option None } } impl CommandTranslator for HuobiCommandTranslator { fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec { topics .iter() .map(|(channel, symbol)| { HuobiCommandTranslator::topic_to_command(channel, symbol, subscribe) }) .collect() } fn translate_to_candlestick_commands( &self, subscribe: bool, symbol_interval_list: &[(String, usize)], ) -> Vec { let topics = symbol_interval_list .iter() .map(|(symbol, interval)| { let channel = Self::to_candlestick_raw_channel(*interval); (channel, symbol.to_string()) }) .collect::>(); self.translate_to_commands(subscribe, &topics) } } #[cfg(test)] mod tests { use crate::common::command_translator::CommandTranslator; #[test] fn test_one_topic() { let translator = super::HuobiCommandTranslator {}; let commands = translator .translate_to_commands(true, &[("trade.detail".to_string(), "btcusdt".to_string())]); assert_eq!(1, commands.len()); assert_eq!(r#"{"sub":"market.btcusdt.trade.detail","id":"crypto-ws-client"}"#, commands[0]); } #[test] fn test_two_topics() { let translator = super::HuobiCommandTranslator {}; let commands = translator.translate_to_commands( true, &[ ("trade.detail".to_string(), "btcusdt".to_string()), ("bbo".to_string(), "btcusdt".to_string()), ], ); assert_eq!(2, commands.len()); assert_eq!(r#"{"sub":"market.btcusdt.trade.detail","id":"crypto-ws-client"}"#, commands[0]); assert_eq!(r#"{"sub":"market.btcusdt.bbo","id":"crypto-ws-client"}"#, commands[1]); } } ================================================ FILE: crypto-ws-client/src/clients/kraken/kraken_futures.rs ================================================ use async_trait::async_trait; use std::collections::HashMap; use tokio_tungstenite::tungstenite::Message; use super::EXCHANGE_NAME; use crate::{ clients::common_traits::{ Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO, }, common::{ command_translator::CommandTranslator, message_handler::{MessageHandler, MiscMessage}, ws_client_internal::WSClientInternal, }, WSClient, }; use log::*; use serde_json::Value; // https://support.kraken.com/hc/en-us/articles/360022839491-API-URLs const WEBSOCKET_URL: &str = "wss://futures.kraken.com/ws/v1"; /// The WebSocket client for Kraken Futures market. /// /// /// * WebSocket API doc: /// * Trading at: pub struct KrakenFuturesWSClient { client: WSClientInternal, translator: KrakenCommandTranslator, } impl_new_constructor!( KrakenFuturesWSClient, EXCHANGE_NAME, WEBSOCKET_URL, KrakenMessageHandler {}, KrakenCommandTranslator {} ); #[rustfmt::skip] impl_trait!(Trade, KrakenFuturesWSClient, subscribe_trade, "trade"); #[rustfmt::skip] impl_trait!(OrderBook, KrakenFuturesWSClient, subscribe_orderbook, "book"); #[rustfmt::skip] impl_trait!(Ticker, KrakenFuturesWSClient, subscribe_ticker, "ticker"); panic_bbo!(KrakenFuturesWSClient); panic_l2_topk!(KrakenFuturesWSClient); panic_l3_orderbook!(KrakenFuturesWSClient); panic_candlestick!(KrakenFuturesWSClient); impl_ws_client_trait!(KrakenFuturesWSClient); struct KrakenMessageHandler {} struct KrakenCommandTranslator {} impl KrakenCommandTranslator { fn channel_symbols_to_command(channel: &str, symbols: &[String], subscribe: bool) -> String { format!( r#"{{"event":"{}","feed":"{}","product_ids":{}}}"#, if subscribe { "subscribe" } else { "unsubscribe" }, channel, serde_json::to_string(symbols).unwrap(), ) } } impl MessageHandler for KrakenMessageHandler { fn handle_message(&mut self, msg: &str) -> MiscMessage { let obj = serde_json::from_str::>(msg).unwrap(); if obj.contains_key("event") { let event = obj.get("event").unwrap().as_str().unwrap(); match event { "error" => panic!("Received {msg} from {EXCHANGE_NAME}"), "info" | "subscribed" | "unsubscribed" => { info!("Received {} from {}", msg, EXCHANGE_NAME); MiscMessage::Other } _ => { warn!("Received {} from {}", msg, EXCHANGE_NAME); MiscMessage::Other } } } else if obj.contains_key("feed") { let feed = obj.get("feed").unwrap().as_str().unwrap(); if feed == "heartbeat" { debug!("Received {} from {}", msg, EXCHANGE_NAME); MiscMessage::WebSocket(Message::Ping(Vec::new())) } else if obj.contains_key("product_id") { MiscMessage::Normal } else { MiscMessage::Other } } else { MiscMessage::Other } } fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)> { // In order to keep the websocket connection alive, you will need to // make a ping request at least every 60 seconds. // https://support.kraken.com/hc/en-us/articles/360022635632-Subscriptions-WebSockets-API- None // TODO: lack of doc } } impl CommandTranslator for KrakenCommandTranslator { fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec { let mut commands: Vec = Vec::new(); let mut channel_symbols = HashMap::>::new(); for (channel, symbol) in topics { match channel_symbols.get_mut(channel) { Some(symbols) => symbols.push(symbol.to_string()), None => { channel_symbols.insert(channel.to_string(), vec![symbol.to_string()]); } } } for (channel, symbols) in channel_symbols.iter() { commands.push(Self::channel_symbols_to_command(channel, symbols, subscribe)); } commands.push(r#"{"event":"subscribe","feed":"heartbeat"}"#.to_string()); commands } fn translate_to_candlestick_commands( &self, _subscribe: bool, _symbol_interval_list: &[(String, usize)], ) -> Vec { panic!("Kraken Futures does NOT have candlestick channel"); } } #[cfg(test)] mod tests { use crate::common::command_translator::CommandTranslator; #[test] fn test_one_symbol() { let translator = super::KrakenCommandTranslator {}; let commands = translator .translate_to_commands(true, &[("trade".to_string(), "PI_XBTUSD".to_string())]); assert_eq!(2, commands.len()); assert_eq!( r#"{"event":"subscribe","feed":"trade","product_ids":["PI_XBTUSD"]}"#, commands[0] ); assert_eq!(r#"{"event":"subscribe","feed":"heartbeat"}"#, commands[1]); } #[test] fn test_two_symbols() { let translator = super::KrakenCommandTranslator {}; let commands = translator.translate_to_commands( true, &[ ("trade".to_string(), "PI_XBTUSD".to_string()), ("trade".to_string(), "PI_ETHUSD".to_string()), ], ); assert_eq!(2, commands.len()); assert_eq!( r#"{"event":"subscribe","feed":"trade","product_ids":["PI_XBTUSD","PI_ETHUSD"]}"#, commands[0] ); assert_eq!(r#"{"event":"subscribe","feed":"heartbeat"}"#, commands[1]); } } ================================================ FILE: crypto-ws-client/src/clients/kraken/kraken_spot.rs ================================================ use async_trait::async_trait; use std::collections::HashMap; use tokio_tungstenite::tungstenite::Message; use super::EXCHANGE_NAME; use crate::{ clients::common_traits::{ Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO, }, common::{ command_translator::CommandTranslator, message_handler::{MessageHandler, MiscMessage}, ws_client_internal::WSClientInternal, }, WSClient, }; use log::*; use serde_json::Value; const WEBSOCKET_URL: &str = "wss://ws.kraken.com"; /// The WebSocket client for Kraken Spot market. /// /// /// * WebSocket API doc: /// * Trading at: pub struct KrakenSpotWSClient { client: WSClientInternal, translator: KrakenCommandTranslator, } impl_new_constructor!( KrakenSpotWSClient, EXCHANGE_NAME, WEBSOCKET_URL, KrakenMessageHandler {}, KrakenCommandTranslator {} ); #[rustfmt::skip] impl_trait!(Trade, KrakenSpotWSClient, subscribe_trade, "trade"); impl_trait!(OrderBook, KrakenSpotWSClient, subscribe_orderbook, "book"); #[rustfmt::skip] impl_trait!(Ticker, KrakenSpotWSClient, subscribe_ticker, "ticker"); #[rustfmt::skip] impl_trait!(BBO, KrakenSpotWSClient, subscribe_bbo, "spread"); impl_candlestick!(KrakenSpotWSClient); panic_l2_topk!(KrakenSpotWSClient); panic_l3_orderbook!(KrakenSpotWSClient); impl_ws_client_trait!(KrakenSpotWSClient); struct KrakenMessageHandler {} struct KrakenCommandTranslator {} impl MessageHandler for KrakenMessageHandler { fn handle_message(&mut self, msg: &str) -> MiscMessage { let resp = serde_json::from_str::(msg); if resp.is_err() { error!("{} is not a JSON string, {}", msg, EXCHANGE_NAME); return MiscMessage::Other; } let value = resp.unwrap(); if value.is_object() { let obj = value.as_object().unwrap(); let event = obj.get("event").unwrap().as_str().unwrap(); match event { "heartbeat" => { debug!("Received {} from {}", msg, EXCHANGE_NAME); let ping = r#"{ "event": "ping", "reqid": 9527 }"#; MiscMessage::WebSocket(Message::Text(ping.to_string())) } "pong" => MiscMessage::Pong, "subscriptionStatus" => { let status = obj.get("status").unwrap().as_str().unwrap(); match status { "subscribed" | "unsubscribed" => { info!("Received {} from {}", msg, EXCHANGE_NAME) } "error" => { let error_msg = obj.get("errorMessage").unwrap().as_str().unwrap(); if error_msg.starts_with("Currency pair not supported") { // Sometimes currency pairs returned from RESTful API don't exist in // WebSocket yet error!("Received {} from {}", msg, EXCHANGE_NAME) } else { panic!("Received {msg} from {EXCHANGE_NAME}"); } } _ => warn!("Received {} from {}", msg, EXCHANGE_NAME), } MiscMessage::Other } "systemStatus" => { let status = obj.get("status").unwrap().as_str().unwrap(); match status { "maintenance" | "cancel_only" => { warn!("Received {}, which means Kraken is in maintenance mode", msg); std::thread::sleep(std::time::Duration::from_secs(20)); MiscMessage::Reconnect } _ => { info!("Received {} from {}", msg, EXCHANGE_NAME); MiscMessage::Other } } } _ => { warn!("Received {} from {}", msg, EXCHANGE_NAME); MiscMessage::Other } } } else { MiscMessage::Normal } } fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)> { // Client can ping server to determine whether connection is alive // https://docs.kraken.com/websockets/#message-ping Some((Message::Text(r#"{"event":"ping"}"#.to_string()), 10)) } } impl KrakenCommandTranslator { fn name_symbols_to_command(name: &str, symbols: &[String], subscribe: bool) -> String { if name == "book" { format!( r#"{{"event":"{}","pair":{},"subscription":{{"name":"{}","depth":25}}}}"#, if subscribe { "subscribe" } else { "unsubscribe" }, serde_json::to_string(symbols).unwrap(), name ) } else { format!( r#"{{"event":"{}","pair":{},"subscription":{{"name":"{}"}}}}"#, if subscribe { "subscribe" } else { "unsubscribe" }, serde_json::to_string(symbols).unwrap(), name ) } } fn convert_symbol_interval_list( symbol_interval_list: &[(String, usize)], ) -> Vec<(Vec, usize)> { let mut map = HashMap::>::new(); for task in symbol_interval_list { let v = map.entry(task.1).or_insert_with(Vec::new); v.push(task.0.clone()); } let mut result = Vec::new(); for (k, v) in map { result.push((v, k)); } result } } impl CommandTranslator for KrakenCommandTranslator { fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec { let mut commands: Vec = Vec::new(); let mut channel_symbols = HashMap::>::new(); for (channel, symbol) in topics { match channel_symbols.get_mut(channel) { Some(symbols) => symbols.push(symbol.to_string()), None => { channel_symbols.insert(channel.to_string(), vec![symbol.to_string()]); } } } for (channel, symbols) in channel_symbols.iter() { commands.push(Self::name_symbols_to_command(channel, symbols, subscribe)); } commands } fn translate_to_candlestick_commands( &self, subscribe: bool, symbol_interval_list: &[(String, usize)], ) -> Vec { let valid_set: Vec = vec![1, 5, 15, 30, 60, 240, 1440, 10080, 21600].into_iter().map(|x| x * 60).collect(); let invalid_intervals = symbol_interval_list .iter() .map(|(_, interval)| *interval) .filter(|x| !valid_set.contains(x)) .collect::>(); if !invalid_intervals.is_empty() { panic!( "Invalid intervals: {}, available intervals: {}", invalid_intervals .into_iter() .map(|x| x.to_string()) .collect::>() .join(","), valid_set.into_iter().map(|x| x.to_string()).collect::>().join(",") ); } let symbols_interval_list = Self::convert_symbol_interval_list(symbol_interval_list); let commands: Vec = symbols_interval_list .into_iter() .map(|(symbols, interval)| { format!( r#"{{"event":"{}","pair":{},"subscription":{{"name":"ohlc", "interval":{}}}}}"#, if subscribe { "subscribe" } else { "unsubscribe" }, serde_json::to_string(&symbols).unwrap(), interval / 60 ) }) .collect(); commands } } #[cfg(test)] mod tests { use crate::common::command_translator::CommandTranslator; #[test] fn test_one_symbol() { let translator = super::KrakenCommandTranslator {}; let commands = translator.translate_to_commands(true, &[("trade".to_string(), "XBT/USD".to_string())]); assert_eq!(1, commands.len()); assert_eq!( r#"{"event":"subscribe","pair":["XBT/USD"],"subscription":{"name":"trade"}}"#, commands[0] ); } #[test] fn test_two_symbols() { let translator = super::KrakenCommandTranslator {}; let commands = translator.translate_to_commands( true, &[ ("trade".to_string(), "XBT/USD".to_string()), ("trade".to_string(), "ETH/USD".to_string()), ], ); assert_eq!(1, commands.len()); assert_eq!( r#"{"event":"subscribe","pair":["XBT/USD","ETH/USD"],"subscription":{"name":"trade"}}"#, commands[0] ); } } ================================================ FILE: crypto-ws-client/src/clients/kraken/mod.rs ================================================ mod kraken_futures; mod kraken_spot; const EXCHANGE_NAME: &str = "kraken"; pub use kraken_futures::KrakenFuturesWSClient; pub use kraken_spot::KrakenSpotWSClient; ================================================ FILE: crypto-ws-client/src/clients/kucoin/kucoin_spot.rs ================================================ use super::utils::{fetch_ws_token, KucoinMessageHandler, EXCHANGE_NAME, UPLINK_LIMIT}; use crate::{ clients::common_traits::{ Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO, }, common::{command_translator::CommandTranslator, ws_client_internal::WSClientInternal}, WSClient, }; use async_trait::async_trait; use std::sync::mpsc::Sender; /// The WebSocket client for KuCoin Spot market. /// /// * WebSocket API doc: /// * Trading at: pub struct KuCoinSpotWSClient { client: WSClientInternal, translator: KucoinCommandTranslator, } impl KuCoinSpotWSClient { /// Creates a KuCoinSpotWSClient websocket client. /// /// # Arguments /// /// * `tx` - The sending part of a channel /// * `url` - Optional server url, usually you don't need specify it pub async fn new(tx: Sender, url: Option<&str>) -> Self { let real_url = match url { Some(endpoint) => endpoint.to_string(), None => { let ws_token = fetch_ws_token().await; let ws_url = format!("{}?token={}", ws_token.endpoint, ws_token.token); ws_url } }; KuCoinSpotWSClient { client: WSClientInternal::connect( EXCHANGE_NAME, &real_url, KucoinMessageHandler {}, Some(UPLINK_LIMIT), tx, ) .await, translator: KucoinCommandTranslator {}, } } } impl_trait!(Trade, KuCoinSpotWSClient, subscribe_trade, "/market/match"); impl_trait!(BBO, KuCoinSpotWSClient, subscribe_bbo, "/market/ticker"); #[rustfmt::skip] impl_trait!(OrderBook, KuCoinSpotWSClient, subscribe_orderbook, "/market/level2"); #[rustfmt::skip] impl_trait!(OrderBookTopK, KuCoinSpotWSClient, subscribe_orderbook_topk, "/spotMarket/level2Depth5"); #[rustfmt::skip] impl_trait!(Ticker, KuCoinSpotWSClient, subscribe_ticker, "/market/snapshot"); impl_candlestick!(KuCoinSpotWSClient); panic_l3_orderbook!(KuCoinSpotWSClient); impl_ws_client_trait!(KuCoinSpotWSClient); struct KucoinCommandTranslator {} impl KucoinCommandTranslator { fn to_candlestick_channel(symbol: &str, interval: usize) -> String { let interval_str = match interval { 60 => "1min", 180 => "3min", 300 => "5min", 900 => "15min", 1800 => "30min", 3600 => "1hour", 7200 => "2hour", 14400 => "4hour", 21600 => "6hour", 28800 => "8hour", 43200 => "12hour", 86400 => "1day", 604800 => "1week", _ => panic!( "KuCoin available intervals 1min,3min,5min,15min,30min,1hour,2hour,4hour,6hour,8hour,12hour,1day,1week" ), }; format!("{symbol}_{interval_str}") } } impl CommandTranslator for KucoinCommandTranslator { fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec { super::utils::topics_to_commands(topics, subscribe) } fn translate_to_candlestick_commands( &self, subscribe: bool, symbol_interval_list: &[(String, usize)], ) -> Vec { let topics = symbol_interval_list .iter() .map(|(symbol, interval)| { ("/market/candles".to_string(), Self::to_candlestick_channel(symbol, *interval)) }) .collect::>(); self.translate_to_commands(subscribe, &topics) } } #[cfg(test)] mod tests { use crate::common::command_translator::CommandTranslator; #[test] fn test_one_channel() { let translator = super::KucoinCommandTranslator {}; let commands = translator .translate_to_commands(true, &[("/market/match".to_string(), "BTC-USDT".to_string())]); assert_eq!(1, commands.len()); assert_eq!( r#"{"id":"crypto-ws-client","type":"subscribe","topic":"/market/match:BTC-USDT","privateChannel":false,"response":true}"#, commands[0] ); let commands = translator.translate_to_commands( true, &[ ("/market/match".to_string(), "BTC-USDT".to_string()), ("/market/match".to_string(), "ETH-USDT".to_string()), ], ); assert_eq!(1, commands.len()); assert_eq!( r#"{"id":"crypto-ws-client","type":"subscribe","topic":"/market/match:BTC-USDT,ETH-USDT","privateChannel":false,"response":true}"#, commands[0] ); } #[test] fn test_two_channels() { let translator = super::KucoinCommandTranslator {}; let commands = translator.translate_to_commands( true, &[ ("/market/match".to_string(), "BTC-USDT".to_string()), ("/market/level2".to_string(), "ETH-USDT".to_string()), ], ); assert_eq!(2, commands.len()); assert_eq!( r#"{"id":"crypto-ws-client","type":"subscribe","topic":"/market/level2:ETH-USDT","privateChannel":false,"response":true}"#, commands[0] ); assert_eq!( r#"{"id":"crypto-ws-client","type":"subscribe","topic":"/market/match:BTC-USDT","privateChannel":false,"response":true}"#, commands[1] ); let commands = translator.translate_to_commands( true, &[ ("/market/match".to_string(), "BTC-USDT".to_string()), ("/market/match".to_string(), "ETH-USDT".to_string()), ("/market/level2".to_string(), "BTC-USDT".to_string()), ("/market/level2".to_string(), "ETH-USDT".to_string()), ], ); assert_eq!(2, commands.len()); assert_eq!( r#"{"id":"crypto-ws-client","type":"subscribe","topic":"/market/level2:BTC-USDT,ETH-USDT","privateChannel":false,"response":true}"#, commands[0] ); assert_eq!( r#"{"id":"crypto-ws-client","type":"subscribe","topic":"/market/match:BTC-USDT,ETH-USDT","privateChannel":false,"response":true}"#, commands[1] ); } #[test] fn test_candlestick() { let translator = super::KucoinCommandTranslator {}; let commands = translator.translate_to_candlestick_commands( true, &[("BTC-USDT".to_string(), 180), ("ETH-USDT".to_string(), 60)], ); assert_eq!(1, commands.len()); assert_eq!( r#"{"id":"crypto-ws-client","type":"subscribe","topic":"/market/candles:BTC-USDT_3min,ETH-USDT_1min","privateChannel":false,"response":true}"#, commands[0] ); } } ================================================ FILE: crypto-ws-client/src/clients/kucoin/kucoin_swap.rs ================================================ use super::utils::{fetch_ws_token, KucoinMessageHandler, EXCHANGE_NAME, UPLINK_LIMIT}; use crate::{ clients::common_traits::{ Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO, }, common::{command_translator::CommandTranslator, ws_client_internal::WSClientInternal}, WSClient, }; use async_trait::async_trait; use std::sync::mpsc::Sender; /// The WebSocket client for KuCoin Swap markets. /// /// * WebSocket API doc: /// * Trading at: pub struct KuCoinSwapWSClient { client: WSClientInternal, translator: KucoinCommandTranslator, } impl KuCoinSwapWSClient { /// Creates a KuCoinSwapWSClient websocket client. /// /// # Arguments /// /// * `tx` - The sending part of a channel /// * `url` - Optional server url, usually you don't need specify it pub async fn new(tx: Sender, url: Option<&str>) -> Self { let real_url = match url { Some(endpoint) => endpoint.to_string(), None => { let ws_token = fetch_ws_token().await; let ws_url = format!("{}?token={}", ws_token.endpoint, ws_token.token); ws_url } }; KuCoinSwapWSClient { client: WSClientInternal::connect( EXCHANGE_NAME, &real_url, KucoinMessageHandler {}, Some(UPLINK_LIMIT), tx, ) .await, translator: KucoinCommandTranslator {}, } } } #[rustfmt::skip] impl_trait!(Trade, KuCoinSwapWSClient, subscribe_trade, "/contractMarket/execution"); #[rustfmt::skip] impl_trait!(BBO, KuCoinSwapWSClient, subscribe_bbo, "/contractMarket/tickerV2"); #[rustfmt::skip] impl_trait!(OrderBook, KuCoinSwapWSClient, subscribe_orderbook, "/contractMarket/level2"); #[rustfmt::skip] impl_trait!(OrderBookTopK, KuCoinSwapWSClient, subscribe_orderbook_topk, "/contractMarket/level2Depth5"); #[rustfmt::skip] impl_trait!(Ticker, KuCoinSwapWSClient, subscribe_ticker, "/contractMarket/snapshot"); impl_candlestick!(KuCoinSwapWSClient); panic_l3_orderbook!(KuCoinSwapWSClient); impl_ws_client_trait!(KuCoinSwapWSClient); struct KucoinCommandTranslator {} impl KucoinCommandTranslator { fn to_candlestick_channel(symbol: &str, interval: usize) -> String { let valid_set: Vec = vec![60, 300, 900, 1800, 3600, 7200, 14400, 28800, 43200, 86400, 604800]; if !valid_set.contains(&interval) { let joined = valid_set.into_iter().map(|x| x.to_string()).collect::>().join(","); panic!("KuCoin Swap available intervals {joined}"); } format!("{}_{}", symbol, interval / 60) } } impl CommandTranslator for KucoinCommandTranslator { fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec { super::utils::topics_to_commands(topics, subscribe) } fn translate_to_candlestick_commands( &self, subscribe: bool, symbol_interval_list: &[(String, usize)], ) -> Vec { let topics = symbol_interval_list .iter() .map(|(symbol, interval)| { ( "/contractMarket/candle".to_string(), Self::to_candlestick_channel(symbol, *interval), ) }) .collect::>(); self.translate_to_commands(subscribe, &topics) } } #[cfg(test)] mod tests { use crate::common::command_translator::CommandTranslator; #[test] fn test_one_channel() { let translator = super::KucoinCommandTranslator {}; let commands = translator.translate_to_commands( true, &[("/contractMarket/execution".to_string(), "BTC_USD".to_string())], ); assert_eq!(1, commands.len()); assert_eq!( r#"{"id":"crypto-ws-client","type":"subscribe","topic":"/contractMarket/execution:BTC_USD","privateChannel":false,"response":true}"#, commands[0] ); let commands = translator.translate_to_commands( true, &[ ("/contractMarket/execution".to_string(), "BTC_USD".to_string()), ("/contractMarket/execution".to_string(), "ETH_USD".to_string()), ], ); assert_eq!(1, commands.len()); assert_eq!( r#"{"id":"crypto-ws-client","type":"subscribe","topic":"/contractMarket/execution:BTC_USD,ETH_USD","privateChannel":false,"response":true}"#, commands[0] ); } #[test] fn test_two_channels() { let translator = super::KucoinCommandTranslator {}; let commands = translator.translate_to_commands( true, &[ ("/contractMarket/execution".to_string(), "BTC_USD".to_string()), ("/contractMarket/level2".to_string(), "ETH_USD".to_string()), ], ); assert_eq!(2, commands.len()); assert_eq!( r#"{"id":"crypto-ws-client","type":"subscribe","topic":"/contractMarket/execution:BTC_USD","privateChannel":false,"response":true}"#, commands[0] ); assert_eq!( r#"{"id":"crypto-ws-client","type":"subscribe","topic":"/contractMarket/level2:ETH_USD","privateChannel":false,"response":true}"#, commands[1] ); let commands = translator.translate_to_commands( true, &[ ("/contractMarket/execution".to_string(), "BTC_USD".to_string()), ("/contractMarket/execution".to_string(), "ETH_USD".to_string()), ("/contractMarket/level2".to_string(), "BTC_USD".to_string()), ("/contractMarket/level2".to_string(), "ETH_USD".to_string()), ], ); assert_eq!(2, commands.len()); assert_eq!( r#"{"id":"crypto-ws-client","type":"subscribe","topic":"/contractMarket/execution:BTC_USD,ETH_USD","privateChannel":false,"response":true}"#, commands[0] ); assert_eq!( r#"{"id":"crypto-ws-client","type":"subscribe","topic":"/contractMarket/level2:BTC_USD,ETH_USD","privateChannel":false,"response":true}"#, commands[1] ); } #[test] fn test_candlestick() { let translator = super::KucoinCommandTranslator {}; let commands = translator.translate_to_candlestick_commands( true, &[("BTC_USD".to_string(), 300), ("ETH_USD".to_string(), 60)], ); assert_eq!(1, commands.len()); assert_eq!( r#"{"id":"crypto-ws-client","type":"subscribe","topic":"/contractMarket/candle:BTC_USD_5,ETH_USD_1","privateChannel":false,"response":true}"#, commands[0] ); } } ================================================ FILE: crypto-ws-client/src/clients/kucoin/mod.rs ================================================ mod kucoin_spot; mod kucoin_swap; mod utils; pub use kucoin_spot::KuCoinSpotWSClient; pub use kucoin_swap::KuCoinSwapWSClient; ================================================ FILE: crypto-ws-client/src/clients/kucoin/utils.rs ================================================ use std::{ collections::{BTreeMap, HashMap}, num::NonZeroU32, }; use log::*; use nonzero_ext::nonzero; use reqwest::{header, Result}; use serde_json::Value; use tokio_tungstenite::tungstenite::Message; use crate::common::message_handler::{MessageHandler, MiscMessage}; pub(super) const EXCHANGE_NAME: &str = "kucoin"; // Maximum number of batch subscriptions at a time: 100 topics // See https://docs.kucoin.com/#topic-subscription-limit const MAX_TOPICS_PER_COMMAND: usize = 100; // Message limit sent to the server: 100 per 10 seconds, see https://docs.kucoin.cc/#request-rate-limit pub(super) const UPLINK_LIMIT: (NonZeroU32, std::time::Duration) = (nonzero!(100u32), std::time::Duration::from_secs(10)); pub(super) struct WebsocketToken { pub token: String, pub endpoint: String, } async fn http_post(url: &str) -> Result { let mut headers = header::HeaderMap::new(); headers.insert(header::CONTENT_TYPE, header::HeaderValue::from_static("application/json")); let client = reqwest::Client::builder() .default_headers(headers) .user_agent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36") .gzip(true) .build()?; let response = client.post(url).send().await?; match response.error_for_status() { Ok(resp) => Ok(resp.text().await?), Err(error) => Err(error), } } // See pub(super) async fn fetch_ws_token() -> WebsocketToken { let txt = http_post("https://openapi-v2.kucoin.com/api/v1/bullet-public").await.unwrap(); let obj = serde_json::from_str::>(&txt).unwrap(); let code = obj.get("code").unwrap().as_str().unwrap(); if code != "200000" { panic!("Failed to get token, code is {code}"); } let data = obj.get("data").unwrap().as_object().unwrap(); let token = data.get("token").unwrap().as_str().unwrap(); let servers = data.get("instanceServers").unwrap().as_array().unwrap(); let server = servers[0].as_object().unwrap(); WebsocketToken { token: token.to_string(), endpoint: server.get("endpoint").unwrap().as_str().unwrap().to_string(), } } fn channel_symbols_to_command(channel: &str, symbols: &[String], subscribe: bool) -> String { format!( r#"{{"id":"crypto-ws-client","type":"{}","topic":"{}:{}","privateChannel":false,"response":true}}"#, if subscribe { "subscribe" } else { "unsubscribe" }, channel, symbols.join(",") ) } pub(super) fn topics_to_commands(topics: &[(String, String)], subscribe: bool) -> Vec { let mut commands: Vec = Vec::new(); let mut channel_symbols = BTreeMap::>::new(); for (channel, symbol) in topics { match channel_symbols.get_mut(channel) { Some(symbols) => symbols.push(symbol.to_string()), None => { channel_symbols.insert(channel.to_string(), vec![symbol.to_string()]); } } } for (channel, symbols) in channel_symbols { let mut chunk: Vec = Vec::new(); for symbol in symbols { if chunk.len() >= MAX_TOPICS_PER_COMMAND { commands.push(channel_symbols_to_command(&channel, &chunk, subscribe)); chunk.clear(); } chunk.push(symbol); } if !chunk.is_empty() { commands.push(channel_symbols_to_command(&channel, &chunk, subscribe)); } } commands } pub(super) struct KucoinMessageHandler {} impl MessageHandler for KucoinMessageHandler { fn handle_message(&mut self, msg: &str) -> MiscMessage { let obj = serde_json::from_str::>(msg).unwrap(); let msg_type = obj.get("type").unwrap().as_str().unwrap(); match msg_type { "pong" => MiscMessage::Pong, "welcome" | "ack" => { debug!("Received {} from {}", msg, EXCHANGE_NAME); MiscMessage::Other } "notice" | "command" => { info!("Received {} from {}", msg, EXCHANGE_NAME); MiscMessage::Other } "message" => MiscMessage::Normal, "error" => { panic!("Received {msg} from {EXCHANGE_NAME}"); } _ => { panic!("Received {msg} from {EXCHANGE_NAME}"); } } } fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)> { // See: // - https://docs.kucoin.com/#ping // - https://docs.kucoin.cc/futures/#ping // // If the server has not received the ping from the client for 60 seconds , the // connection will be disconnected. Some((Message::Text(r#"{"type":"ping", "id": "crypto-ws-client"}"#.to_string()), 60)) } } #[cfg(test)] mod tests { #[tokio::test(flavor = "multi_thread")] async fn fetch_ws_token() { let ws_token = super::fetch_ws_token().await; assert!(!ws_token.token.is_empty()) } #[test] fn test_topics_to_commands() { let commands = super::topics_to_commands( &[("/market/match".to_string(), "BTC-USDT".to_string())], true, ); assert_eq!(1, commands.len()); } } ================================================ FILE: crypto-ws-client/src/clients/mexc/mexc_spot.rs ================================================ use async_trait::async_trait; use std::collections::HashMap; use tokio_tungstenite::tungstenite::Message; use super::EXCHANGE_NAME; use crate::{ clients::common_traits::{ Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO, }, common::{ command_translator::CommandTranslator, message_handler::{MessageHandler, MiscMessage}, ws_client_internal::WSClientInternal, }, WSClient, }; use log::*; use serde_json::Value; pub(super) const SPOT_WEBSOCKET_URL: &str = "wss://wbs.mexc.com/raw/ws"; /// MEXC Spot market. /// /// * WebSocket API doc: /// * Trading at: pub struct MexcSpotWSClient { client: WSClientInternal, translator: MexcCommandTranslator, } impl_new_constructor!( MexcSpotWSClient, EXCHANGE_NAME, SPOT_WEBSOCKET_URL, MexcMessageHandler {}, MexcCommandTranslator {} ); #[rustfmt::skip] impl_trait!(Trade, MexcSpotWSClient, subscribe_trade, "deal"); #[rustfmt::skip] impl_trait!(OrderBook, MexcSpotWSClient, subscribe_orderbook, "depth"); #[rustfmt::skip] impl_trait!(OrderBookTopK, MexcSpotWSClient, subscribe_orderbook_topk, "limit.depth"); impl_candlestick!(MexcSpotWSClient); panic_bbo!(MexcSpotWSClient); panic_ticker!(MexcSpotWSClient); panic_l3_orderbook!(MexcSpotWSClient); impl_ws_client_trait!(MexcSpotWSClient); struct MexcMessageHandler {} struct MexcCommandTranslator {} impl MessageHandler for MexcMessageHandler { fn handle_message(&mut self, msg: &str) -> MiscMessage { if msg == "pong" { return MiscMessage::Pong; } if let Ok(obj) = serde_json::from_str::>(msg) { if obj.contains_key("channel") && obj.contains_key("data") { let channel = obj.get("channel").unwrap().as_str().unwrap(); match channel { "push.deal" | "push.depth" | "push.limit.depth" | "push.kline" => { if obj.contains_key("symbol") { MiscMessage::Normal } else { warn!("Received {} from {}", msg, EXCHANGE_NAME); MiscMessage::Other } } "push.overview" => MiscMessage::Normal, _ => { warn!("Received {} from {}", msg, EXCHANGE_NAME); MiscMessage::Other } } } else { warn!("Received {} from {}", msg, EXCHANGE_NAME); MiscMessage::Other } } else { warn!("Received {} from {}", msg, EXCHANGE_NAME); MiscMessage::Other } } fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)> { Some((Message::Text("ping".to_string()), 5)) } } impl MexcCommandTranslator { fn topic_to_command(channel: &str, symbol: &str, subscribe: bool) -> String { if channel == "limit.depth" { format!( r#"{{"op":"{}.{}","symbol":"{}","depth": 5}}"#, if subscribe { "sub" } else { "unsub" }, channel, symbol ) } else { format!( r#"{{"op":"{}.{}","symbol":"{}"}}"#, if subscribe { "sub" } else { "unsub" }, channel, symbol ) } } fn interval_to_string(interval: usize) -> String { let tmp = match interval { 60 => "Min1", 300 => "Min5", 900 => "Min15", 1800 => "Min30", 3600 => "Min60", 14400 => "Hour4", 28800 => "Hour8", 86400 => "Day1", 604800 => "Week1", 2592000 => "Month1", _ => panic!( "MEXC has intervals Min1,Min5,Min15,Min30,Min60,Hour4,Hour8,Day1,Week1,Month1" ), }; tmp.to_string() } } impl CommandTranslator for MexcCommandTranslator { fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec { topics .iter() .map(|(channel, symbol)| { MexcCommandTranslator::topic_to_command(channel, symbol, subscribe) }) .collect() } fn translate_to_candlestick_commands( &self, subscribe: bool, symbol_interval_list: &[(String, usize)], ) -> Vec { symbol_interval_list .iter() .map(|(symbol, interval)| { format!( r#"{{"op":"{}.kline","symbol":"{}","interval":"{}"}}"#, if subscribe { "sub" } else { "unsub" }, symbol, Self::interval_to_string(*interval) ) }) .collect() } } #[cfg(test)] mod tests { use crate::common::command_translator::CommandTranslator; #[test] fn test_one_topic() { let translator = super::MexcCommandTranslator {}; let commands = translator.translate_to_commands(true, &[("deal".to_string(), "BTC_USDT".to_string())]); assert_eq!(1, commands.len()); assert_eq!(r#"{"op":"sub.deal","symbol":"BTC_USDT"}"#, commands[0]); } #[test] fn test_two_topic() { let translator = super::MexcCommandTranslator {}; let commands = translator.translate_to_commands( true, &[ ("deal".to_string(), "BTC_USDT".to_string()), ("depth".to_string(), "ETH_USDT".to_string()), ], ); assert_eq!(2, commands.len()); assert_eq!(r#"{"op":"sub.deal","symbol":"BTC_USDT"}"#, commands[0]); assert_eq!(r#"{"op":"sub.depth","symbol":"ETH_USDT"}"#, commands[1]); } #[test] fn test_candlestick() { let translator = super::MexcCommandTranslator {}; let commands = translator.translate_to_candlestick_commands(true, &[("BTC_USDT".to_string(), 60)]); assert_eq!(1, commands.len()); assert_eq!(r#"{"op":"sub.kline","symbol":"BTC_USDT","interval":"Min1"}"#, commands[0]); } } ================================================ FILE: crypto-ws-client/src/clients/mexc/mexc_swap.rs ================================================ use async_trait::async_trait; use std::collections::HashMap; use tokio_tungstenite::tungstenite::Message; use super::EXCHANGE_NAME; use crate::{ clients::common_traits::{ Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO, }, common::{ command_translator::CommandTranslator, message_handler::{MessageHandler, MiscMessage}, ws_client_internal::WSClientInternal, }, WSClient, }; use log::*; use serde_json::Value; pub(super) const SWAP_WEBSOCKET_URL: &str = "wss://contract.mexc.com/ws"; /// MEXC Swap market. /// /// * WebSocket API doc: /// * Trading at: pub struct MexcSwapWSClient { client: WSClientInternal, translator: MexcCommandTranslator, } impl_new_constructor!( MexcSwapWSClient, EXCHANGE_NAME, SWAP_WEBSOCKET_URL, MexcMessageHandler {}, MexcCommandTranslator {} ); #[rustfmt::skip] impl_trait!(Trade, MexcSwapWSClient, subscribe_trade, "deal"); #[rustfmt::skip] impl_trait!(Ticker, MexcSwapWSClient, subscribe_ticker, "ticker"); #[rustfmt::skip] impl_trait!(OrderBook, MexcSwapWSClient, subscribe_orderbook, "depth"); #[rustfmt::skip] impl_trait!(OrderBookTopK, MexcSwapWSClient, subscribe_orderbook_topk, "depth.full"); impl_candlestick!(MexcSwapWSClient); panic_bbo!(MexcSwapWSClient); panic_l3_orderbook!(MexcSwapWSClient); impl_ws_client_trait!(MexcSwapWSClient); struct MexcMessageHandler {} struct MexcCommandTranslator {} impl MessageHandler for MexcMessageHandler { fn handle_message(&mut self, msg: &str) -> MiscMessage { let obj = serde_json::from_str::>(msg).unwrap(); if obj.contains_key("channel") && obj.contains_key("data") && obj.contains_key("ts") { let channel = obj.get("channel").unwrap().as_str().unwrap(); match channel { "pong" => MiscMessage::Pong, "rs.error" => { error!("Received {} from {}", msg, EXCHANGE_NAME); panic!("Received {msg} from {EXCHANGE_NAME}"); } _ => { if obj.contains_key("symbol") && channel.starts_with("push.") { MiscMessage::Normal } else { info!("Received {} from {}", msg, EXCHANGE_NAME); MiscMessage::Other } } } } else { error!("Received {} from {}", msg, SWAP_WEBSOCKET_URL); MiscMessage::Other } } fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)> { // more than 60 seconds no response, close the channel Some((Message::Text(r#"{"method":"ping"}"#.to_string()), 60)) } } impl MexcCommandTranslator { fn topic_to_command(channel: &str, symbol: &str, subscribe: bool) -> String { format!( r#"{{"method":"{}.{}","param":{{"symbol":"{}"}}}}"#, if subscribe { "sub" } else { "unsub" }, channel, symbol ) } fn interval_to_string(interval: usize) -> String { let tmp = match interval { 60 => "Min1", 300 => "Min5", 900 => "Min15", 1800 => "Min30", 3600 => "Min60", 14400 => "Hour4", 28800 => "Hour8", 86400 => "Day1", 604800 => "Week1", 2592000 => "Month1", _ => panic!( "MEXC has intervals Min1,Min5,Min15,Min30,Min60,Hour4,Hour8,Day1,Week1,Month1" ), }; tmp.to_string() } } impl CommandTranslator for MexcCommandTranslator { fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec { topics .iter() .map(|(channel, symbol)| { MexcCommandTranslator::topic_to_command(channel, symbol, subscribe) }) .collect() } fn translate_to_candlestick_commands( &self, subscribe: bool, symbol_interval_list: &[(String, usize)], ) -> Vec { symbol_interval_list .iter() .map(|(symbol, interval)| { format!( r#"{{"method":"{}.kline","param":{{"symbol":"{}","interval":"{}"}}}}"#, if subscribe { "sub" } else { "unsub" }, symbol, Self::interval_to_string(*interval) ) }) .collect() } } #[cfg(test)] mod tests { use crate::common::command_translator::CommandTranslator; #[test] fn test_one_topic() { let translator = super::MexcCommandTranslator {}; let commands = translator.translate_to_commands(true, &[("deal".to_string(), "BTC_USDT".to_string())]); assert_eq!(1, commands.len()); assert_eq!(r#"{"method":"sub.deal","param":{"symbol":"BTC_USDT"}}"#, commands[0]); } #[test] fn test_two_topic() { let translator = super::MexcCommandTranslator {}; let commands = translator.translate_to_commands( true, &[ ("deal".to_string(), "BTC_USDT".to_string()), ("depth".to_string(), "ETH_USDT".to_string()), ], ); assert_eq!(2, commands.len()); assert_eq!(r#"{"method":"sub.deal","param":{"symbol":"BTC_USDT"}}"#, commands[0]); assert_eq!(r#"{"method":"sub.depth","param":{"symbol":"ETH_USDT"}}"#, commands[1]); } #[test] fn test_candlestick() { let translator = super::MexcCommandTranslator {}; let commands = translator.translate_to_candlestick_commands(true, &[("BTC_USDT".to_string(), 60)]); assert_eq!(1, commands.len()); assert_eq!( r#"{"method":"sub.kline","param":{"symbol":"BTC_USDT","interval":"Min1"}}"#, commands[0] ); } } ================================================ FILE: crypto-ws-client/src/clients/mexc/mod.rs ================================================ mod mexc_spot; mod mexc_swap; pub(super) const EXCHANGE_NAME: &str = "mexc"; pub use mexc_spot::MexcSpotWSClient; pub use mexc_swap::MexcSwapWSClient; ================================================ FILE: crypto-ws-client/src/clients/mod.rs ================================================ #[macro_use] pub(super) mod common_traits; pub(super) mod binance; pub(super) mod binance_option; pub(super) mod bitfinex; pub(super) mod bitget; pub(super) mod bithumb; pub(super) mod bitmex; pub(super) mod bitstamp; pub(super) mod bitz; pub(super) mod bybit; pub(super) mod coinbase_pro; pub(super) mod deribit; pub(super) mod dydx; pub(super) mod ftx; pub(super) mod gate; pub(super) mod huobi; pub(super) mod kraken; pub(super) mod kucoin; pub(super) mod mexc; pub(super) mod okx; pub(super) mod zb; pub(super) mod zbg; ================================================ FILE: crypto-ws-client/src/clients/okx.rs ================================================ use async_trait::async_trait; use nonzero_ext::nonzero; use std::{ collections::{BTreeMap, HashMap}, num::NonZeroU32, }; use tokio_tungstenite::tungstenite::Message; use log::*; use serde_json::Value; use crate::{ clients::common_traits::{ Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO, }, common::{ command_translator::CommandTranslator, message_handler::{MessageHandler, MiscMessage}, utils::ensure_frame_size, ws_client_internal::WSClientInternal, }, WSClient, }; pub(crate) const EXCHANGE_NAME: &str = "okx"; const WEBSOCKET_URL: &str = "wss://ws.okx.com:8443/ws/v5/public"; /// https://www.okx.com/docs-v5/en/#websocket-api-subscribe /// The total length of multiple channels cannot exceed 4096 bytes const WS_FRAME_SIZE: usize = 4096; // Subscription limit: 240 times per hour // see https://www.okx.com/docs-v5/en/#websocket-api-connect const UPLINK_LIMIT: (NonZeroU32, std::time::Duration) = (nonzero!(240u32), std::time::Duration::from_secs(3600)); /// The WebSocket client for OKX. /// /// OKX has Spot, Future, Swap and Option markets. /// /// * API doc: /// * Trading at: /// * Spot /// * Future /// * Swap /// * Option pub struct OkxWSClient { client: WSClientInternal, translator: OkxCommandTranslator, } impl OkxWSClient { pub async fn new(tx: std::sync::mpsc::Sender, url: Option<&str>) -> Self { let real_url = match url { Some(endpoint) => endpoint, None => WEBSOCKET_URL, }; OkxWSClient { client: WSClientInternal::connect( EXCHANGE_NAME, real_url, OkxMessageHandler {}, Some(UPLINK_LIMIT), tx, ) .await, translator: OkxCommandTranslator {}, } } } impl_trait!(Trade, OkxWSClient, subscribe_trade, "trades"); impl_trait!(Ticker, OkxWSClient, subscribe_ticker, "tickers"); impl_trait!(BBO, OkxWSClient, subscribe_bbo, "bbo-tbt"); #[rustfmt::skip] // books-l2-tbt and books50-l2-tbt require login, only books doesn't require it impl_trait!(OrderBook, OkxWSClient, subscribe_orderbook, "books"); #[rustfmt::skip] impl_trait!(OrderBookTopK, OkxWSClient, subscribe_orderbook_topk, "books5"); impl_candlestick!(OkxWSClient); panic_l3_orderbook!(OkxWSClient); impl_ws_client_trait!(OkxWSClient); struct OkxMessageHandler {} struct OkxCommandTranslator {} impl OkxCommandTranslator { fn topics_to_command(chunk: &[(String, String)], subscribe: bool) -> String { let arr = chunk .iter() .map(|t| { let mut map = BTreeMap::new(); let (channel, symbol) = t; map.insert("channel".to_string(), channel.to_string()); map.insert("instId".to_string(), symbol.to_string()); map }) .collect::>>(); format!( r#"{{"op":"{}","args":{}}}"#, if subscribe { "subscribe" } else { "unsubscribe" }, serde_json::to_string(&arr).unwrap(), ) } // see https://www.okx.com/docs-v5/en/#websocket-api-public-channel-candlesticks-channel fn to_candlestick_raw_channel(interval: usize) -> &'static str { match interval { 60 => "candle1m", 180 => "candle3m", 300 => "candle5m", 900 => "candle15m", 1800 => "candle30m", 3600 => "candle1H", 7200 => "candle2H", 14400 => "candle4H", 21600 => "candle6H", 43200 => "candle12H", 86400 => "candle1D", 172800 => "candle2D", 259200 => "candle3D", 432000 => "candle5D", 604800 => "candle1W", 2592000 => "candle1M", _ => panic!("Invalid OKX candlestick interval {interval}"), } } } impl MessageHandler for OkxMessageHandler { fn handle_message(&mut self, msg: &str) -> MiscMessage { if msg == "pong" { return MiscMessage::Pong; } let resp = serde_json::from_str::>(msg); if resp.is_err() { error!("{} is not a JSON string, {}", msg, EXCHANGE_NAME); return MiscMessage::Other; } let obj = resp.unwrap(); if let Some(event) = obj.get("event") { match event.as_str().unwrap() { "error" => { let error_code = obj.get("code").unwrap().as_str().unwrap().parse::().unwrap(); match error_code { 30040 => { // channel doesn't exist, ignore because some symbols don't exist in // websocket while they exist in `/v3/instruments` error!("Received {} from {}", msg, EXCHANGE_NAME); } _ => panic!("Received {msg} from {EXCHANGE_NAME}"), } } "subscribe" => info!("Received {} from {}", msg, EXCHANGE_NAME), "unsubscribe" => info!("Received {} from {}", msg, EXCHANGE_NAME), _ => warn!("Received {} from {}", msg, EXCHANGE_NAME), } MiscMessage::Other } else if !obj.contains_key("arg") || !obj.contains_key("data") { error!("Received {} from {}", msg, EXCHANGE_NAME); MiscMessage::Other } else { MiscMessage::Normal } } fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)> { // https://www.okx.com/docs-v5/en/#websocket-api-connect Some((Message::Text("ping".to_string()), 30)) } } impl CommandTranslator for OkxCommandTranslator { fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec { ensure_frame_size(topics, subscribe, Self::topics_to_command, WS_FRAME_SIZE, None) } fn translate_to_candlestick_commands( &self, subscribe: bool, symbol_interval_list: &[(String, usize)], ) -> Vec { let topics = symbol_interval_list .iter() .map(|(symbol, interval)| { let channel = Self::to_candlestick_raw_channel(*interval); (channel.to_string(), symbol.to_string()) }) .collect::>(); self.translate_to_commands(subscribe, &topics) } } #[cfg(test)] mod tests { use crate::common::command_translator::CommandTranslator; #[test] fn test_one_topic() { let translator = super::OkxCommandTranslator {}; let commands = translator .translate_to_commands(true, &[("trades".to_string(), "BTC-USDT".to_string())]); assert_eq!(1, commands.len()); assert_eq!( r#"{"op":"subscribe","args":[{"channel":"trades","instId":"BTC-USDT"}]}"#, commands[0] ); } #[test] fn test_two_topics() { let translator = super::OkxCommandTranslator {}; let commands = translator.translate_to_commands( true, &[ ("trades".to_string(), "BTC-USDT".to_string()), ("tickers".to_string(), "BTC-USDT".to_string()), ], ); assert_eq!(1, commands.len()); assert_eq!( r#"{"op":"subscribe","args":[{"channel":"trades","instId":"BTC-USDT"},{"channel":"tickers","instId":"BTC-USDT"}]}"#, commands[0] ); } } ================================================ FILE: crypto-ws-client/src/clients/zb/mod.rs ================================================ mod zb_spot; mod zb_swap; pub use zb_spot::ZbSpotWSClient; pub use zb_swap::ZbSwapWSClient; const EXCHANGE_NAME: &str = "zb"; ================================================ FILE: crypto-ws-client/src/clients/zb/zb_spot.rs ================================================ use std::collections::HashMap; use async_trait::async_trait; use log::*; use serde_json::Value; use tokio_tungstenite::tungstenite::Message; use crate::{ clients::common_traits::{ Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO, }, common::{ command_translator::CommandTranslator, message_handler::{MessageHandler, MiscMessage}, ws_client_internal::WSClientInternal, }, WSClient, }; use super::EXCHANGE_NAME; // If you're in China, use wss://api.zbex.site/websocket instead const WEBSOCKET_URL: &str = "wss://api.zb.com/websocket"; /// The WebSocket client for ZB spot market. /// /// * WebSocket API doc: /// * Trading at: pub struct ZbSpotWSClient { client: WSClientInternal, translator: ZbCommandTranslator, } impl_new_constructor!( ZbSpotWSClient, EXCHANGE_NAME, WEBSOCKET_URL, ZbMessageHandler {}, ZbCommandTranslator {} ); #[rustfmt::skip] impl_trait!(Trade, ZbSpotWSClient, subscribe_trade, "trades"); #[rustfmt::skip] impl_trait!(OrderBookTopK, ZbSpotWSClient, subscribe_orderbook_topk, "depth"); #[rustfmt::skip] impl_trait!(Ticker, ZbSpotWSClient, subscribe_ticker, "ticker"); impl_candlestick!(ZbSpotWSClient); panic_bbo!(ZbSpotWSClient); panic_l2!(ZbSpotWSClient); panic_l3_orderbook!(ZbSpotWSClient); impl_ws_client_trait!(ZbSpotWSClient); struct ZbMessageHandler {} struct ZbCommandTranslator {} impl MessageHandler for ZbMessageHandler { fn handle_message(&mut self, msg: &str) -> MiscMessage { let obj = serde_json::from_str::>(msg).unwrap(); let channel = obj["channel"].as_str().unwrap(); if channel == "pong" { return MiscMessage::Pong; } if let Some(code) = obj.get("code") { let code = code.as_i64().unwrap(); if code != 1000 { if code == 1007 { panic!("Received {msg} from {EXCHANGE_NAME}"); } else { error!("Received {} from {}", msg, EXCHANGE_NAME); } return MiscMessage::Other; } } MiscMessage::Normal } fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)> { Some((Message::Text(r#"{"channel":"ping","event":"addChannel"}"#.to_string()), 3)) } } impl ZbCommandTranslator { fn to_candlestick_raw_channel(&self, symbol: &str, interval: usize) -> String { let interval_str = match interval { 60 => "1min", 180 => "3min", 300 => "5min", 900 => "15min", 1800 => "30min", 3600 => "1hour", 7200 => "2hour", 14400 => "4hour", 21600 => "6hour", 43200 => "12hour", 86400 => "1day", 259200 => "3day", 604800 => "1week", _ => panic!( "ZB spot available intervals: 1week, 3day, 1day, 12hour, 6hour, 4hour, 2hour, 1hour, 30min, 15min, 5min, 3min, 1min" ), }; format!("{}_kline_{}", symbol.replace('_', ""), interval_str,) } } impl CommandTranslator for ZbCommandTranslator { fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec { topics .iter() .map(|(channel, symbol)| { format!( r#"{{"event":"{}","channel":"{}_{}"}}"#, if subscribe { "addChannel" } else { "removeChannel" }, symbol.replace('_', ""), channel, ) }) .collect() } fn translate_to_candlestick_commands( &self, subscribe: bool, symbol_interval_list: &[(String, usize)], ) -> Vec { symbol_interval_list .iter() .map(|(symbol, interval)| { format!( r#"{{"event":"{}","channel":"{}"}}"#, if subscribe { "addChannel" } else { "removeChannel" }, self.to_candlestick_raw_channel(symbol, *interval), ) }) .collect() } } #[cfg(test)] mod tests { use crate::common::command_translator::CommandTranslator; #[tokio::test(flavor = "multi_thread")] async fn test_one_topic() { let translator = super::ZbCommandTranslator {}; let commands = translator .translate_to_commands(true, &[("trades".to_string(), "btc_usdt".to_string())]); assert_eq!(1, commands.len()); assert_eq!(r#"{"event":"addChannel","channel":"btcusdt_trades"}"#, commands[0]); } #[tokio::test(flavor = "multi_thread")] async fn test_two_topic() { let translator = super::ZbCommandTranslator {}; let commands = translator.translate_to_commands( true, &[ ("trades".to_string(), "btc_usdt".to_string()), ("depth".to_string(), "eth_usdt".to_string()), ], ); assert_eq!(2, commands.len()); assert_eq!(r#"{"event":"addChannel","channel":"btcusdt_trades"}"#, commands[0]); assert_eq!(r#"{"event":"addChannel","channel":"ethusdt_depth"}"#, commands[1]); } #[tokio::test(flavor = "multi_thread")] async fn test_candlestick() { let translator = super::ZbCommandTranslator {}; let commands = translator.translate_to_candlestick_commands(true, &[("btc_usdt".to_string(), 60)]); assert_eq!(1, commands.len()); assert_eq!(r#"{"event":"addChannel","channel":"btcusdt_kline_1min"}"#, commands[0]); } } ================================================ FILE: crypto-ws-client/src/clients/zb/zb_swap.rs ================================================ use std::{collections::HashMap, num::NonZeroU32}; use async_trait::async_trait; use nonzero_ext::nonzero; use serde_json::Value; use tokio_tungstenite::tungstenite::Message; use crate::{ clients::common_traits::{ Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO, }, common::{ command_translator::CommandTranslator, message_handler::{MessageHandler, MiscMessage}, ws_client_internal::WSClientInternal, }, WSClient, }; use log::*; use super::EXCHANGE_NAME; const WEBSOCKET_URL: &str = "wss://fapi.zb.com/ws/public/v1"; // The default limit for the number of requests for a single interface is 200 // times/2s // // See https://github.com/ZBFuture/docs/blob/main/API%20V2%20_en.md#14-access-limit-frequency-rules const UPLINK_LIMIT: (NonZeroU32, std::time::Duration) = (nonzero!(200u32), std::time::Duration::from_secs(2)); /// The WebSocket client for ZB swap market. /// /// * WebSocket API doc: /// * Trading at: pub struct ZbSwapWSClient { client: WSClientInternal, translator: ZbCommandTranslator, } impl ZbSwapWSClient { pub async fn new(tx: std::sync::mpsc::Sender, url: Option<&str>) -> Self { let real_url = match url { Some(endpoint) => endpoint, None => WEBSOCKET_URL, }; ZbSwapWSClient { client: WSClientInternal::connect( EXCHANGE_NAME, real_url, ZbMessageHandler {}, Some(UPLINK_LIMIT), tx, ) .await, translator: ZbCommandTranslator {}, } } } #[rustfmt::skip] impl_trait!(Trade, ZbSwapWSClient, subscribe_trade, "Trade"); #[rustfmt::skip] impl_trait!(OrderBook, ZbSwapWSClient, subscribe_orderbook, "Depth"); #[rustfmt::skip] impl_trait!(OrderBookTopK, ZbSwapWSClient, subscribe_orderbook_topk, "DepthWhole"); #[rustfmt::skip] impl_trait!(Ticker, ZbSwapWSClient, subscribe_ticker, "Ticker"); impl_candlestick!(ZbSwapWSClient); panic_bbo!(ZbSwapWSClient); panic_l3_orderbook!(ZbSwapWSClient); impl_ws_client_trait!(ZbSwapWSClient); struct ZbMessageHandler {} struct ZbCommandTranslator {} impl MessageHandler for ZbMessageHandler { fn handle_message(&mut self, msg: &str) -> MiscMessage { if msg == r#"{"action":"pong"}"# { return MiscMessage::Pong; } if msg.contains("error") { error!("Received {} from {}", msg, EXCHANGE_NAME); return MiscMessage::Other; } let obj = serde_json::from_str::>(msg).unwrap(); if obj.contains_key("channel") && obj.contains_key("data") { MiscMessage::Normal } else { warn!("Received {} from {}", msg, EXCHANGE_NAME); MiscMessage::Other } } fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)> { // https://github.com/ZBFuture/docs/blob/main/API%20V2%20_en.md#812-ping Some((Message::Text(r#"{"action":"ping"}"#.to_string()), 10)) } } impl ZbCommandTranslator { fn to_candlestick_raw_channel(&self, symbol: &str, interval: usize) -> String { let interval_str = match interval { 60 => "1M", 300 => "5M", 900 => "15M", 1800 => "30M", 3600 => "1H", 21600 => "6H", 86400 => "1D", 432000 => "5D", _ => panic!("ZB swap available intervals: 1M,5M,15M, 30M, 1H, 6H, 1D, 5D"), }; format!("{symbol}.KLine_{interval_str}",) } } impl CommandTranslator for ZbCommandTranslator { fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec { let action = if subscribe { "subscribe" } else { "unsubscribe" }; topics .iter() .map(|(channel, symbol)| match channel.as_str() { "Trade" => format!( r#"{{"action":"{action}", "channel":"{symbol}.{channel}", "size":100}}"#, ), "Depth" => format!( r#"{{"action":"{action}", "channel":"{symbol}.{channel}", "size":200}}"#, ), "DepthWhole" => format!( r#"{{"action":"{action}", "channel":"{symbol}.{channel}", "size":10}}"#, ), "Ticker" => { format!(r#"{{"action":"{action}", "channel":"{symbol}.{channel}"}}"#,) } _ => panic!("Unknown ZB channel {channel}"), }) .collect() } fn translate_to_candlestick_commands( &self, subscribe: bool, symbol_interval_list: &[(String, usize)], ) -> Vec { let action = if subscribe { "subscribe" } else { "unsubscribe" }; symbol_interval_list .iter() .map(|(symbol, interval)| { format!( r#"{{"action":"{}", "channel":"{}", "size":1}}"#, action, self.to_candlestick_raw_channel(symbol, *interval), ) }) .collect() } } #[cfg(test)] mod tests { use crate::common::command_translator::CommandTranslator; #[tokio::test(flavor = "multi_thread")] async fn test_one_topic() { let translator = super::ZbCommandTranslator {}; let commands = translator .translate_to_commands(true, &[("Trade".to_string(), "BTC_USDT".to_string())]); assert_eq!(1, commands.len()); assert_eq!( r#"{"action":"subscribe", "channel":"BTC_USDT.Trade", "size":100}"#, commands[0] ); } #[tokio::test(flavor = "multi_thread")] async fn test_two_topic() { let translator = super::ZbCommandTranslator {}; let commands = translator.translate_to_commands( true, &[ ("Trade".to_string(), "BTC_USDT".to_string()), ("Depth".to_string(), "ETH_USDT".to_string()), ], ); assert_eq!(2, commands.len()); assert_eq!( r#"{"action":"subscribe", "channel":"BTC_USDT.Trade", "size":100}"#, commands[0] ); assert_eq!( r#"{"action":"subscribe", "channel":"ETH_USDT.Depth", "size":200}"#, commands[1] ); } #[tokio::test(flavor = "multi_thread")] async fn test_candlestick() { let translator = super::ZbCommandTranslator {}; let commands = translator.translate_to_candlestick_commands(true, &[("BTC_USDT".to_string(), 60)]); assert_eq!(1, commands.len()); assert_eq!( r#"{"action":"subscribe", "channel":"BTC_USDT.KLine_1M", "size":1}"#, commands[0] ); } } ================================================ FILE: crypto-ws-client/src/clients/zbg/mod.rs ================================================ mod utils; mod zbg_spot; mod zbg_swap; pub use zbg_spot::ZbgSpotWSClient; pub use zbg_swap::ZbgSwapWSClient; const EXCHANGE_NAME: &str = "zbg"; ================================================ FILE: crypto-ws-client/src/clients/zbg/utils.rs ================================================ use std::collections::HashMap; use reqwest::{header, Result}; use serde_json::Value; async fn http_get(url: &str) -> Result { let mut headers = header::HeaderMap::new(); headers.insert(header::CONTENT_TYPE, header::HeaderValue::from_static("application/json")); let client = reqwest::Client::builder() .default_headers(headers) .user_agent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36") .gzip(true) .build()?; let response = client.get(url).send().await?; match response.error_for_status() { Ok(resp) => Ok(resp.text().await?), Err(error) => Err(error), } } // See https://zbgapi.github.io/docs/spot/v1/en/#public-get-all-supported-trading-symbols pub(super) async fn fetch_symbol_id_map_spot() -> HashMap { let mut symbol_id_map: HashMap = vec![("btc_usdt", 329), ("eth_usdt", 330), ("eos_usdt", 333), ("zb_usdt", 321)] .into_iter() .map(|x| (x.0.to_string(), x.1)) .collect(); if let Ok(txt) = http_get("https://www.zbg.com/exchange/api/v1/common/symbols").await { if let Ok(obj) = serde_json::from_str::>(&txt) { if obj .get("resMsg") .unwrap() .as_object() .unwrap() .get("code") .unwrap() .as_str() .unwrap() == "1" { let arr = obj.get("datas").unwrap().as_array().unwrap(); for v in arr.iter() { let obj = v.as_object().unwrap(); let symbol = obj.get("symbol").unwrap().as_str().unwrap(); let id = obj.get("id").unwrap().as_str().unwrap(); symbol_id_map.insert(symbol.to_string(), id.parse::().unwrap()); } } } } symbol_id_map } // See https://zbgapi.github.io/docs/future/v1/en/#public-get-contracts pub(super) async fn fetch_symbol_contract_id_map_swap() -> HashMap { let mut symbol_contract_id_map: HashMap = vec![ ("BTC_USDT", 1000000), ("BTC_USD-R", 1000001), ("ETH_USDT", 1000002), ("ETH_USD-R", 1000003), ] .into_iter() .map(|x| (x.0.to_string(), x.1)) .collect(); if let Ok(txt) = http_get("https://www.zbg.com/exchange/api/v1/future/common/contracts").await { if let Ok(obj) = serde_json::from_str::>(&txt) { if obj .get("resMsg") .unwrap() .as_object() .unwrap() .get("code") .unwrap() .as_str() .unwrap() == "1" { let arr = obj.get("datas").unwrap().as_array().unwrap(); for v in arr.iter() { let obj = v.as_object().unwrap(); let symbol = obj.get("symbol").unwrap().as_str().unwrap(); let contract_id = obj.get("contractId").unwrap().as_i64().unwrap(); symbol_contract_id_map.insert(symbol.to_string(), contract_id); } } } } symbol_contract_id_map } ================================================ FILE: crypto-ws-client/src/clients/zbg/zbg_spot.rs ================================================ use async_trait::async_trait; use std::collections::HashMap; use tokio_tungstenite::tungstenite::Message; use crate::{ clients::common_traits::{ Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO, }, common::{ command_translator::CommandTranslator, message_handler::{MessageHandler, MiscMessage}, ws_client_internal::WSClientInternal, }, WSClient, }; use super::{utils::fetch_symbol_id_map_spot, EXCHANGE_NAME}; const WEBSOCKET_URL: &str = "wss://kline.zbg.com/websocket"; /// The WebSocket client for ZBG spot market. /// /// * WebSocket API doc: /// * Trading at: pub struct ZbgSpotWSClient { client: WSClientInternal, translator: ZbgCommandTranslator, } impl_new_constructor!( ZbgSpotWSClient, EXCHANGE_NAME, WEBSOCKET_URL, ZbgMessageHandler {}, ZbgCommandTranslator::new().await ); #[rustfmt::skip] impl_trait!(Trade, ZbgSpotWSClient, subscribe_trade, "TRADE"); #[rustfmt::skip] impl_trait!(OrderBook, ZbgSpotWSClient, subscribe_orderbook, "ENTRUST_ADD"); #[rustfmt::skip] impl_trait!(Ticker, ZbgSpotWSClient, subscribe_ticker, "TRADE_STATISTIC_24H"); impl_candlestick!(ZbgSpotWSClient); panic_bbo!(ZbgSpotWSClient); panic_l2_topk!(ZbgSpotWSClient); panic_l3_orderbook!(ZbgSpotWSClient); impl_ws_client_trait!(ZbgSpotWSClient); struct ZbgMessageHandler {} struct ZbgCommandTranslator { symbol_id_map: HashMap, } impl MessageHandler for ZbgMessageHandler { fn handle_message(&mut self, msg: &str) -> MiscMessage { if msg.contains(r#"action":"PING"#) { MiscMessage::Pong } else { MiscMessage::Normal } } fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)> { Some((Message::Text(r#"{"action":"PING"}"#.to_string()), 10)) } } impl ZbgCommandTranslator { async fn new() -> Self { let symbol_id_map = fetch_symbol_id_map_spot().await; ZbgCommandTranslator { symbol_id_map } } fn to_raw_channel(&self, channel: &str, symbol: &str) -> String { let symbol_id = self .symbol_id_map .get(symbol.to_lowercase().as_str()) .unwrap_or_else(|| panic!("Failed to find symbol_id for {symbol}")); if channel == "TRADE_STATISTIC_24H" { format!("{symbol_id}_{channel}") } else { format!("{}_{}_{}", symbol_id, channel, symbol.to_uppercase()) } } fn to_candlestick_raw_channel(&self, symbol: &str, interval: usize) -> String { let interval_str = match interval { 60 => "1M", 300 => "5M", 900 => "15M", 1800 => "30M", 3600 => "1H", 14400 => "4H", 86400 => "1D", 604800 => "1W", _ => panic!("ZBG spot available intervals 1M,5M,15M,30M,1H,4H,1D,1W"), }; let symbol_id = self .symbol_id_map .get(symbol.to_lowercase().as_str()) .unwrap_or_else(|| panic!("Failed to find symbol_id for {symbol}")); format!("{}_KLINE_{}_{}", symbol_id, interval_str, symbol.to_uppercase()) } } impl CommandTranslator for ZbgCommandTranslator { fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec { topics .iter() .map(|(channel, symbol)| { format!( r#"{{"action":"{}", "dataType":{}}}"#, if subscribe { "ADD" } else { "DEL" }, self.to_raw_channel(channel, symbol), ) }) .collect() } fn translate_to_candlestick_commands( &self, subscribe: bool, symbol_interval_list: &[(String, usize)], ) -> Vec { symbol_interval_list .iter() .map(|(symbol, interval)| { format!( r#"{{"action":"{}", "dataType":{}}}"#, if subscribe { "ADD" } else { "DEL" }, self.to_candlestick_raw_channel(symbol, *interval), ) }) .collect() } } #[cfg(test)] mod tests { use crate::common::command_translator::CommandTranslator; #[tokio::test(flavor = "multi_thread")] async fn test_one_topic() { let translator = super::ZbgCommandTranslator::new().await; let commands = translator .translate_to_commands(true, &[("TRADE".to_string(), "btc_usdt".to_string())]); assert_eq!(1, commands.len()); assert_eq!(r#"{"action":"ADD", "dataType":329_TRADE_BTC_USDT}"#, commands[0]); } #[tokio::test(flavor = "multi_thread")] async fn test_two_topic() { let translator = super::ZbgCommandTranslator::new().await; let commands = translator.translate_to_commands( true, &[ ("TRADE".to_string(), "btc_usdt".to_string()), ("ENTRUST_ADD".to_string(), "eth_usdt".to_string()), ], ); assert_eq!(2, commands.len()); assert_eq!(r#"{"action":"ADD", "dataType":329_TRADE_BTC_USDT}"#, commands[0]); assert_eq!(r#"{"action":"ADD", "dataType":330_ENTRUST_ADD_ETH_USDT}"#, commands[1]); } #[tokio::test(flavor = "multi_thread")] async fn test_candlestick() { let translator = super::ZbgCommandTranslator::new().await; let commands = translator.translate_to_candlestick_commands(true, &[("btc_usdt".to_string(), 60)]); assert_eq!(1, commands.len()); assert_eq!(r#"{"action":"ADD", "dataType":329_KLINE_1M_BTC_USDT}"#, commands[0]); } } ================================================ FILE: crypto-ws-client/src/clients/zbg/zbg_swap.rs ================================================ use async_trait::async_trait; use std::collections::HashMap; use tokio_tungstenite::tungstenite::Message; use crate::{ clients::common_traits::{ Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO, }, common::{ command_translator::CommandTranslator, message_handler::{MessageHandler, MiscMessage}, ws_client_internal::WSClientInternal, }, WSClient, }; use log::*; use super::{utils::fetch_symbol_contract_id_map_swap, EXCHANGE_NAME}; const WEBSOCKET_URL: &str = "wss://kline.zbg.com/exchange/v1/futurews"; /// The WebSocket client for ZBG swap market. /// /// * WebSocket API doc: , /// there is no English doc /// * Trading at: pub struct ZbgSwapWSClient { client: WSClientInternal, translator: ZbgCommandTranslator, } impl_new_constructor!( ZbgSwapWSClient, EXCHANGE_NAME, WEBSOCKET_URL, ZbgMessageHandler {}, ZbgCommandTranslator::new().await ); #[rustfmt::skip] impl_trait!(Trade, ZbgSwapWSClient, subscribe_trade, "future_tick"); #[rustfmt::skip] impl_trait!(OrderBook, ZbgSwapWSClient, subscribe_orderbook, "future_snapshot_depth"); #[rustfmt::skip] impl_trait!(Ticker, ZbgSwapWSClient, subscribe_ticker, "future_snapshot_indicator"); impl_candlestick!(ZbgSwapWSClient); panic_bbo!(ZbgSwapWSClient); panic_l2_topk!(ZbgSwapWSClient); panic_l3_orderbook!(ZbgSwapWSClient); impl_ws_client_trait!(ZbgSwapWSClient); struct ZbgMessageHandler {} struct ZbgCommandTranslator { symbol_id_map: HashMap, } impl MessageHandler for ZbgMessageHandler { fn handle_message(&mut self, msg: &str) -> MiscMessage { if msg == "Pong" { return MiscMessage::Pong; } if msg.starts_with('[') { MiscMessage::Normal } else { warn!("Received {} from {}", msg, EXCHANGE_NAME); MiscMessage::Other } } fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)> { Some((Message::Text("PING".to_string()), 25)) } } impl ZbgCommandTranslator { async fn new() -> Self { let symbol_id_map = fetch_symbol_contract_id_map_swap().await; ZbgCommandTranslator { symbol_id_map } } fn to_raw_channel(&self, channel: &str, symbol: &str) -> String { let contract_id = self .symbol_id_map .get(symbol) .unwrap_or_else(|| panic!("Failed to find contract_id for {symbol}")); format!("{channel}-{contract_id}") } fn to_candlestick_raw_channel(&self, pair: &str, interval: usize) -> String { let valid_set: Vec = vec![60, 180, 300, 900, 1800, 3600, 7200, 14400, 21600, 43200, 86400, 604800]; if !valid_set.contains(&interval) { let joined = valid_set.into_iter().map(|x| x.to_string()).collect::>().join(","); panic!("ZBG Swap available intervals {joined}"); } let contract_id = self .symbol_id_map .get(pair) .unwrap_or_else(|| panic!("Failed to find contract_id for {pair}")); format!("future_kline-{}-{}", contract_id, interval * 1000) } } impl CommandTranslator for ZbgCommandTranslator { fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec { topics .iter() .map(|(channel, symbol)| { format!( r#"{{"action":"{}", "topic":"{}"}}"#, if subscribe { "sub" } else { "unsub" }, self.to_raw_channel(channel, symbol), ) }) .collect() } fn translate_to_candlestick_commands( &self, subscribe: bool, symbol_interval_list: &[(String, usize)], ) -> Vec { symbol_interval_list .iter() .map(|(symbol, interval)| { format!( r#"{{"action":"{}", "topic":"{}"}}"#, if subscribe { "sub" } else { "unsub" }, self.to_candlestick_raw_channel(symbol, *interval), ) }) .collect() } } #[cfg(test)] mod tests { use crate::common::command_translator::CommandTranslator; #[tokio::test(flavor = "multi_thread")] async fn test_one_topic() { let translator = super::ZbgCommandTranslator::new().await; let commands = translator .translate_to_commands(true, &[("future_tick".to_string(), "BTC_USDT".to_string())]); assert_eq!(1, commands.len()); assert_eq!(r#"{"action":"sub", "topic":"future_tick-1000000"}"#, commands[0]); } #[tokio::test(flavor = "multi_thread")] async fn test_two_topic() { let translator = super::ZbgCommandTranslator::new().await; let commands = translator.translate_to_commands( true, &[ ("future_tick".to_string(), "BTC_USDT".to_string()), ("future_snapshot_depth".to_string(), "ETH_USDT".to_string()), ], ); assert_eq!(2, commands.len()); assert_eq!(r#"{"action":"sub", "topic":"future_tick-1000000"}"#, commands[0]); assert_eq!(r#"{"action":"sub", "topic":"future_snapshot_depth-1000002"}"#, commands[1]); } #[tokio::test(flavor = "multi_thread")] async fn test_candlestick() { let translator = super::ZbgCommandTranslator::new().await; let commands = translator.translate_to_candlestick_commands(true, &[("BTC_USDT".to_string(), 60)]); assert_eq!(1, commands.len()); assert_eq!(r#"{"action":"sub", "topic":"future_kline-1000000-60000"}"#, commands[0]); } } ================================================ FILE: crypto-ws-client/src/common/command_translator.rs ================================================ /// Translate to exchange-specific websocket subscribe/unsubscribe commands. /// /// topic = channel + symbol /// /// A command is a JSON string which can be aceepted by the websocket server, /// and every exchange has its own format. pub(crate) trait CommandTranslator { fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec; fn translate_to_candlestick_commands( &self, subscribe: bool, symbol_interval_list: &[(String, usize)], ) -> Vec; } ================================================ FILE: crypto-ws-client/src/common/connect_async.rs ================================================ use fast_socks5::client::{Config, Socks5Stream}; use futures_util::{SinkExt, StreamExt}; use governor::{Quota, RateLimiter}; use log::*; use nonzero_ext::*; use reqwest::Url; use std::{env, num::NonZeroU32}; use tokio::{ io::{AsyncRead, AsyncWrite}, sync::mpsc::{Receiver, Sender}, }; use tokio_tungstenite::{ tungstenite::{Error, Message}, MaybeTlsStream, WebSocketStream, }; /// Wraps a websocket client inside an event loop, returns a message_rx to /// receive messages and a command_tx to send commands to the websocket server. /// /// To close the websocket connection, send a `Message::Close` message to the /// command_tx. /// /// `limit`, max number of uplink messsages, for example, 100 per 10 seconds pub async fn connect_async( url: &str, uplink_limit: Option<(NonZeroU32, std::time::Duration)>, ) -> Result<(Receiver, Sender), Error> { if let Ok(proxy_env) = env::var("https_proxy").or_else(|_| env::var("http_proxy")) { let proxy_url = Url::parse(&proxy_env).unwrap(); let proxy_scheme = proxy_url.scheme().to_lowercase(); if proxy_scheme.as_str() != "socks5" { panic!("Unsupported proxy scheme {proxy_scheme}"); } let proxy_addr = format!( "{}:{}", proxy_url.host_str().unwrap(), proxy_url.port_or_known_default().unwrap() ); let connect_url = Url::parse(url).unwrap(); let proxy_stream = Socks5Stream::connect( proxy_addr.to_string(), connect_url.host_str().unwrap().to_string(), connect_url.port_or_known_default().unwrap(), Config::default(), ) .await .unwrap(); let (ws_stream, _) = tokio_tungstenite::client_async_tls(connect_url, proxy_stream).await?; // replaced // let ret = tokio_tungstenite::connect_async(url).await; connect_async_internal(ws_stream, uplink_limit).await } else { let (ws_stream, _) = tokio_tungstenite::connect_async(url).await?; connect_async_internal(ws_stream, uplink_limit).await } } async fn connect_async_internal( ws_stream: WebSocketStream>, uplink_limit: Option<(NonZeroU32, std::time::Duration)>, ) -> Result<(Receiver, Sender), Error> { let (command_tx, mut command_rx) = tokio::sync::mpsc::channel::(1); let (message_tx, message_rx) = tokio::sync::mpsc::channel::(32); let (mut write, mut read) = ws_stream.split(); let limiter = if let Some((max_burst, duration)) = uplink_limit { let quota = Quota::with_period(duration).unwrap().allow_burst(max_burst); RateLimiter::direct(quota) } else { RateLimiter::direct(Quota::per_second(nonzero!(u32::max_value()))) }; tokio::task::spawn(async move { loop { tokio::select! { command = command_rx.recv() => { match command { Some(command) => { match command { Message::Close(resp) => { match resp { Some(frame) => { warn!( "Received a CloseFrame: code: {}, reason: {}", frame.code, frame.reason ); } None => warn!("Received an empty close message"), } break; // close the connection and break the loop } _ => { limiter.until_ready().await; if let Err(err) =write.send(command).await { error!("Failed to send, error: {}", err); } } } } None => { debug!("command_rx closed"); break; } } } msg = read.next() => match msg { Some(Ok(msg)) => { let _= message_tx.send(msg).await; } Some(Err(err)) => { error!("Failed to read, error: {}", err); break; } None => { debug!("message_tx closed"); break; } } }; } _ = write.send(Message::Close(None)).await; }); Ok((message_rx, command_tx)) } ================================================ FILE: crypto-ws-client/src/common/message_handler.rs ================================================ use tokio_tungstenite::tungstenite::Message; #[derive(Debug)] pub(crate) enum MiscMessage { Normal, // A normal websocket message which contains a JSON string Mutated(String), // A JSON string mutated by a handler, e.g., bitfinex WebSocket(Message), // WebSocket message that needs to be sent to the server Pong, // Pong message from the server Reconnect, // Needs to reconnect Other, // Other messages will be ignored } /// Exchange-specific message handler. pub(crate) trait MessageHandler { /// Given a message from the exchange, return a MiscMessage which will be /// procesed in run(). fn handle_message(&mut self, msg: &str) -> MiscMessage; /// To keep the connection alive, how often should the client send a ping? /// None means the client doesn't need to send ping, instead the server will /// send ping and the client just needs to reply a pong fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)>; } ================================================ FILE: crypto-ws-client/src/common/mod.rs ================================================ pub(crate) mod command_translator; pub(crate) mod connect_async; pub(crate) mod message_handler; pub(super) mod utils; pub(crate) mod ws_client; pub(super) mod ws_client_internal; ================================================ FILE: crypto-ws-client/src/common/utils.rs ================================================ /// Ensure that length of a websocket message does not exceed the max size or /// the number of topics does not exceed the threshold. pub(crate) fn ensure_frame_size( topics: &[(String, String)], subscribe: bool, topics_to_command: fn(&[(String, String)], bool) -> String, max_bytes: usize, max_topics_per_command: Option, ) -> Vec { let mut all_commands: Vec = Vec::new(); let mut begin = 0; while begin < topics.len() { for end in (begin + 1)..(topics.len() + 1) { let num_subscriptions = end - begin; let chunk = &topics[begin..end]; let command = topics_to_command(chunk, subscribe); if end == topics.len() { all_commands.push(command); begin = end; } else if num_subscriptions >= max_topics_per_command.unwrap_or(usize::MAX) { all_commands.push(command); begin = end; break; } else { let chunk = &topics[begin..end + 1]; let command_next = topics_to_command(chunk, subscribe); if command_next.len() > max_bytes { all_commands.push(command); begin = end; break; } }; } } all_commands } pub(crate) fn topic_to_raw_channel(topic: &(String, String)) -> String { topic.0.replace("SYMBOL", topic.1.as_str()) } ================================================ FILE: crypto-ws-client/src/common/ws_client.rs ================================================ use async_trait::async_trait; /// The public interface of every WebSocket client. #[async_trait] pub trait WSClient { /// Subscribes to trade channels. /// /// A trade channel sends tick-by-tick trade data, which is the complete /// copy of a market's trading information. /// /// Each exchange has its own symbol formats, for example: /// /// * BitMEX `XBTUSD`, `XBTM21` /// * Binance `btcusdt`, `btcusd_perp` /// * OKEx `BTC-USDT` async fn subscribe_trade(&self, symbols: &[String]); /// Subscribes to BBO(best bid & offer) channels. /// /// BBO represents best bid and offer, which is also refered to as level1 /// data. It is the top 1 bid and ask from the orginal orderbook, so BBO /// is updated per tick and non-aggregated. /// /// Not all exchanges have the BBO channel, calling this function with /// these exchanges will panic. /// /// * Binance, BitMEX, Huobi and Kraken have BBO directly. /// * Bitfinex uses `book` channel with `len=1` and `prec="R0"` to get BBO /// data. async fn subscribe_bbo(&self, symbols: &[String]); /// Subscribes to incremental level2 orderbook channels. /// /// An incremental level2 orderbook channel sends a snapshot followed by /// tick-by-tick updates. /// /// Level2 orderbook is the raw orderbook(Level3) aggregated by price level, /// it is also refered to as "market by price level" data. /// /// This function subscribes to exchange specific channels as the following: /// /// * Binance `depth` /// * Bitfinex `book` channel with `prec=P0`, `frec=F0` and `len=25` /// * BitMEX `orderBookL2_25` /// * Bitstamp `diff_order_book`, top 100 /// * CoinbasePro `level2` /// * Huobi `depth.size_20.high_freq` with `data_type=incremental` for /// contracts, `mbp.20` for Spot /// * Kraken `book` with `depth=25` /// * MEXC `depth` for Swap, `symbol` for Spot /// * OKEx `depth_l2_tbt`, top 100 async fn subscribe_orderbook(&self, symbols: &[String]); /// Subscribes to level2 orderbook snapshot channels. /// /// A level2 orderbook snapshot channel sends a complete snapshot every /// interval. /// /// This function subscribes to exchange specific channels as the following: /// /// * Binance `depth5`, every 1000ms /// * Bitfinex has no snapshot channel /// * BitMEX `orderBook10`, top 10, every tick /// * Bitstamp `order_book`, top 10, every 100ms /// * CoinbasePro has no snapshot channel /// * Huobi `depth.step1` and `depth.step7`, top 20, every 1s /// * Kraken has no snapshot channel /// * MEXC `depth.full` for Swap, top 20, every 100ms; `get.depth` for Spot, /// full, every 26s /// * OKEx `depth5`, top 5, every 100ms async fn subscribe_orderbook_topk(&self, symbols: &[String]); /// Subscribes to level3 orderebook channels. /// /// **Only bitfinex, bitstamp, coinbase_pro and kucoin have level3 orderbook /// channels.** /// /// The level3 orderbook is the orginal orderbook of an exchange, it is /// non-aggregated by price level and updated tick-by-tick. async fn subscribe_l3_orderbook(&self, symbols: &[String]); /// Subscribes to ticker channels. /// /// A ticker channel pushes realtime 24hr rolling window ticker messages, /// which contains OHLCV information. /// /// Not all exchanges have the ticker channel, for example, BitMEX, /// Bitstamp, MEXC Spot, etc. async fn subscribe_ticker(&self, symbols: &[String]); /// Subscribes to candlestick channels. /// /// The candlestick channel sends OHLCV messages at interval. /// /// `symbol_interval_list` is a list of symbols and intervals of /// candlesticks in seconds. /// /// Not all exchanges have candlestick channels, for example, Bitstamp /// and CoinbasePro. async fn subscribe_candlestick(&self, symbol_interval_list: &[(String, usize)]); /// Subscribe to multiple topics. /// /// topic = channel + symbol, a topic will be converted to an /// exchange-specific channel(called `raw channel`). /// /// The channel string supports `SYMBOL` as a placeholder, for example, /// `("market.SYMBOL.trade.detail", "btcusdt")` will be converted to a raw /// channel `"market.btcusdt.trade.detail"`. /// /// Examples: /// /// * Binance: `vec![("aggTrade".to_string(), /// "BTCUSDT".to_string()),("ticker".to_string(), "BTCUSDT".to_string())]` /// * Deribit: `vec![(trades.SYMBOL.100ms".to_string(),"BTC-PERPETUAL". /// to_string()),(trades.SYMBOL.100ms".to_string(),"ETH-PERPETUAL". /// to_string())]` /// * Huobi: `vec![("trade.detail".to_string(), /// "btcusdt".to_string()),("trade.detail".to_string(), /// "ethusdt".to_string())]` /// * OKX: `vec![("trades".to_string(), /// "BTC-USDT".to_string()),("trades".to_string(), /// "ETH-USDT".to_string())]` async fn subscribe(&self, topics: &[(String, String)]); /// Unsubscribes multiple topics. /// /// topic = channel + symbol async fn unsubscribe(&self, topics: &[(String, String)]); /// Send raw JSON commands. /// /// This is a low-level API for advanced users only. async fn send(&self, commands: &[String]); /// Starts the infinite event loop. async fn run(&self); /// Close the connection and break the loop in Run(). async fn close(&self); } ================================================ FILE: crypto-ws-client/src/common/ws_client_internal.rs ================================================ use std::{ io::prelude::*, num::NonZeroU32, sync::{ atomic::{AtomicIsize, Ordering}, Arc, }, time::Duration, }; use flate2::read::{DeflateDecoder, GzDecoder}; use log::*; use reqwest::StatusCode; use tokio_tungstenite::tungstenite::{Error, Message}; use crate::common::message_handler::{MessageHandler, MiscMessage}; // `WSClientInternal` should be Sync + Send so that it can be put into Arc // directly. pub(crate) struct WSClientInternal { exchange: &'static str, // Eexchange name pub(crate) url: String, // Websocket base url // pass parameters to run() #[allow(clippy::type_complexity)] params_rx: std::sync::Mutex< tokio::sync::oneshot::Receiver<( H, tokio::sync::mpsc::Receiver, std::sync::mpsc::Sender, )>, >, command_tx: tokio::sync::mpsc::Sender, } impl WSClientInternal { pub async fn connect( exchange: &'static str, url: &str, handler: H, uplink_limit: Option<(NonZeroU32, std::time::Duration)>, tx: std::sync::mpsc::Sender, ) -> Self { // A channel to send parameters to run() let (params_tx, params_rx) = tokio::sync::oneshot::channel::<( H, tokio::sync::mpsc::Receiver, std::sync::mpsc::Sender, )>(); match super::connect_async::connect_async(url, uplink_limit).await { Ok((message_rx, command_tx)) => { let _ = params_tx.send((handler, message_rx, tx)); WSClientInternal { exchange, url: url.to_string(), params_rx: std::sync::Mutex::new(params_rx), command_tx, } } Err(err) => match err { Error::Http(resp) => { if resp.status() == StatusCode::TOO_MANY_REQUESTS { if let Some(retry_after) = resp.headers().get("retry-after") { let mut seconds = retry_after.to_str().unwrap().parse::().unwrap(); seconds += rand::random::() % 9 + 1; // add random seconds to avoid concurrent requests error!( "The retry-after header value is {}, sleeping for {} seconds now", retry_after.to_str().unwrap(), seconds ); tokio::time::sleep(Duration::from_secs(seconds)).await; } } panic!("Failed to connect to {url} due to 429 too many requests") } _ => panic!("Failed to connect to {url}, error: {err}"), }, } } pub async fn send(&self, commands: &[String]) { for command in commands { debug!("{}", command); if self.command_tx.send(Message::Text(command.to_string())).await.is_err() { break; // break the loop if there is no receiver } } } pub async fn run(&self) { let (mut handler, mut message_rx, tx) = { let mut guard = self.params_rx.lock().unwrap(); guard.try_recv().unwrap() }; let num_unanswered_ping = Arc::new(AtomicIsize::new(0)); // for debug only if let Some((msg, interval)) = handler.get_ping_msg_and_interval() { // send heartbeat periodically let command_tx_clone = self.command_tx.clone(); let num_unanswered_ping_clone = num_unanswered_ping.clone(); tokio::task::spawn(async move { let mut timer = { let duration = Duration::from_secs(interval / 2 + 1); tokio::time::interval(duration) }; loop { let now = timer.tick().await; debug!("{:?} sending ping {}", now, msg.to_text().unwrap()); if let Err(err) = command_tx_clone.send(msg.clone()).await { error!("Error sending ping {}", err); } else { num_unanswered_ping_clone.fetch_add(1, Ordering::SeqCst); } } }); } while let Some(msg) = message_rx.recv().await { let txt = match msg { Message::Text(txt) => Some(txt), Message::Binary(binary) => { let mut txt = String::new(); let resp = match self.exchange { crate::clients::huobi::EXCHANGE_NAME | crate::clients::binance::EXCHANGE_NAME | "bitget" | "bitz" => { let mut decoder = GzDecoder::new(&binary[..]); decoder.read_to_string(&mut txt) } crate::clients::okx::EXCHANGE_NAME => { let mut decoder = DeflateDecoder::new(&binary[..]); decoder.read_to_string(&mut txt) } _ => { panic!("Unknown binary format from {}", self.url); } }; match resp { Ok(_) => Some(txt), Err(err) => { error!("Decompression failed, {}", err); None } } } Message::Ping(resp) => { // binance server will send a ping frame every 3 or 5 minutes debug!( "Received a ping frame: {} from {}", std::str::from_utf8(&resp).unwrap(), self.url, ); if self.exchange == "binance" { // send a pong frame debug!("Sending a pong frame to {}", self.url); _ = self.command_tx.send(Message::Pong(Vec::new())).await; } None } Message::Pong(resp) => { num_unanswered_ping.store(0, Ordering::Release); debug!( "Received a pong frame: {} from {}, reset num_unanswered_ping to {}", std::str::from_utf8(&resp).unwrap(), self.exchange, num_unanswered_ping.load(Ordering::Acquire) ); None } Message::Frame(_) => todo!(), Message::Close(resp) => { match resp { Some(frame) => { warn!( "Received a CloseFrame: code: {}, reason: {} from {}", frame.code, frame.reason, self.url ); } None => warn!("Received a close message without CloseFrame"), } // break; panic!("Received a CloseFrame"); //fail fast so that pm2 can restart the process } }; if let Some(txt) = txt { let txt = txt.as_str().trim().to_string(); match handler.handle_message(&txt) { MiscMessage::Normal => { // the receiver might get dropped earlier than this loop if tx.send(txt).is_err() { break; // break the loop if there is no receiver } } MiscMessage::Mutated(txt) => _ = tx.send(txt), MiscMessage::WebSocket(ws_msg) => _ = self.command_tx.send(ws_msg).await, MiscMessage::Pong => { num_unanswered_ping.store(0, Ordering::Release); debug!( "Received {} from {}, reset num_unanswered_ping to {}", txt, self.exchange, num_unanswered_ping.load(Ordering::Acquire) ); } MiscMessage::Reconnect => break, /* fail fast, pm2 will restart, restart is */ // reconnect MiscMessage::Other => (), // ignore } } } } pub async fn close(&self) { // close the websocket connection and break the while loop in run() _ = self.command_tx.send(Message::Close(None)).await; } } ================================================ FILE: crypto-ws-client/src/lib.rs ================================================ //! A versatile websocket client that supports many cryptocurrency exchanges. //! //! ## Example //! //! ``` //! use crypto_ws_client::{BinanceSpotWSClient, WSClient}; //! //! #[tokio::main] //! async fn main() { //! let (tx, rx) = std::sync::mpsc::channel(); //! tokio::task::spawn(async move { //! let symbols = vec!["BTCUSDT".to_string(), "ETHUSDT".to_string()]; //! let ws_client = BinanceSpotWSClient::new(tx, None).await; //! ws_client.subscribe_trade(&symbols).await; //! // run for 5 seconds //! let _ = tokio::time::timeout(std::time::Duration::from_secs(5), ws_client.run()).await; //! ws_client.close(); //! }); //! //! let mut messages = Vec::new(); //! for msg in rx { //! messages.push(msg); //! } //! assert!(!messages.is_empty()); //! } //! ``` //! ## High Level APIs //! //! The following APIs are high-level APIs with ease of use: //! //! * `subscribe_trade(&self, symbols: &[String])` //! * `subscribe_bbo(&self, symbols: &[String])` //! * `subscribe_orderbook(&self, symbols: &[String])` //! * `subscribe_ticker(&self, symbols: &[String])` //! * `subscribe_candlestick(&self, symbol_interval_list: &[(String, usize)])` //! //! They are easier to use and cover most user scenarios. //! //! ## Low Level APIs //! //! Sometimes high-level APIs can NOT meet users' requirements, this package //! provides three low-level APIs: //! //! * `subscribe(&self, topics: &[(String, String)])` //! * `unsubscribe(&self, topics: &[(String, String)])` //! * `send(&self, commands: &[String])` //! //! ## OrderBook Data Categories //! //! Each orderbook has three properties: `aggregation`, `frequency` and `depth`. //! //! | | Aggregated | Non-Aggregated | //! | -------------------- | ----------------- | -------------- | //! | Updated per Tick | Inremental Level2 | Level3 | //! | Updated per Interval | Snapshot Level2 | Not Usefull | //! //! * Level1 data is non-aggregated, updated per tick, top 1 bid & ask from the //! original orderbook. //! * Level2 data is aggregated by price level, updated per tick. //! * Level3 data is the original orderbook, which is not aggregated. mod clients; mod common; pub use common::ws_client::WSClient; pub use clients::{ binance::*, binance_option::*, bitfinex::*, bitget::*, bithumb::*, bitmex::*, bitstamp::*, bitz::*, bybit::*, coinbase_pro::*, deribit::*, dydx::*, ftx::*, gate::*, huobi::*, kraken::*, kucoin::*, mexc::*, okx::*, zb::*, zbg::*, }; ================================================ FILE: crypto-ws-client/tests/binance.rs ================================================ #[macro_use] mod utils; #[cfg(test)] mod binance_spot { use crypto_ws_client::{BinanceSpotWSClient, WSClient}; #[tokio::test(flavor = "multi_thread")] async fn subscribe() { gen_test_code!( BinanceSpotWSClient, subscribe, &[ ("aggTrade".to_string(), "BTCUSDT".to_string()), ("ticker".to_string(), "BTCUSDT".to_string()) ] ); } #[ignore = "!bookTicker has been removed since December 7, 2022"] #[tokio::test(flavor = "multi_thread")] async fn subscribe_all_bbo() { gen_test_code!( BinanceSpotWSClient, send, &[r#"{"id":9527,"method":"SUBSCRIBE","params":["!bookTicker"]}"#.to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_raw_json() { gen_test_code!( BinanceSpotWSClient, send, &[r#"{"id":9527,"method":"SUBSCRIBE","params":["btcusdt@aggTrade","btcusdt@ticker"]}"# .to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_trade() { gen_test_code!( BinanceSpotWSClient, subscribe_trade, &["BTCUSDT".to_string(), "ETHUSDT".to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_ticker() { gen_test_code!( BinanceSpotWSClient, subscribe_ticker, &["BTCUSDT".to_string(), "ETHUSDT".to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_tickers_all() { gen_test_code!( BinanceSpotWSClient, send, &[r#"{"id":9527,"method":"SUBSCRIBE","params":["!ticker@arr"]}"#.to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_bbo() { gen_test_code!( BinanceSpotWSClient, subscribe_bbo, &["BTCUSDT".to_string(), "ETHUSDT".to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook() { gen_test_code!( BinanceSpotWSClient, subscribe_orderbook, &["BTCUSDT".to_string(), "ETHUSDT".to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook_topk() { gen_test_code!( BinanceSpotWSClient, subscribe_orderbook_topk, &["BTCUSDT".to_string(), "ETHUSDT".to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_candlestick() { gen_test_subscribe_candlestick!( BinanceSpotWSClient, &[("BTCUSDT".to_string(), 60), ("ETHUSDT".to_string(), 60)] ); gen_test_subscribe_candlestick!( BinanceSpotWSClient, &[("BTCUSDT".to_string(), 2592000), ("ETHUSDT".to_string(), 2592000)] ); } } #[cfg(test)] mod binance_inverse_future { use crypto_ws_client::{BinanceInverseWSClient, WSClient}; #[tokio::test(flavor = "multi_thread")] async fn subscribe() { gen_test_code!( BinanceInverseWSClient, subscribe, &[ ("aggTrade".to_string(), "BTCUSD_221230".to_string()), ("aggTrade".to_string(), "ETHUSD_221230".to_string()), ("aggTrade".to_string(), "BNBUSD_221230".to_string()) ] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_all_bbo() { gen_test_code!( BinanceInverseWSClient, send, &[r#"{"id":9527,"method":"SUBSCRIBE","params":["!bookTicker"]}"#.to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_trade() { gen_test_code!( BinanceInverseWSClient, subscribe_trade, &[ "BTCUSD_221230".to_string(), "ETHUSD_221230".to_string(), "BNBUSD_221230".to_string() ] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_ticker() { gen_test_code!( BinanceInverseWSClient, subscribe_ticker, &[ "BTCUSD_221230".to_string(), "ETHUSD_221230".to_string(), "BNBUSD_221230".to_string() ] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_tickers_all() { gen_test_code!( BinanceInverseWSClient, send, &[r#"{"id":9527,"method":"SUBSCRIBE","params":["!ticker@arr"]}"#.to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_bbo() { gen_test_code!( BinanceInverseWSClient, subscribe_bbo, &[ "BTCUSD_221230".to_string(), "ETHUSD_221230".to_string(), "BNBUSD_221230".to_string() ] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook() { gen_test_code!( BinanceInverseWSClient, subscribe_orderbook, &[ "BTCUSD_221230".to_string(), "ETHUSD_221230".to_string(), "BNBUSD_221230".to_string() ] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook_topk() { gen_test_code!( BinanceInverseWSClient, subscribe_orderbook_topk, &[ "BTCUSD_221230".to_string(), "ETHUSD_221230".to_string(), "BNBUSD_221230".to_string() ] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_candlestick() { gen_test_subscribe_candlestick!( BinanceInverseWSClient, &[ ("BTCUSD_221230".to_string(), 60), ("ETHUSD_221230".to_string(), 60), ("BNBUSD_221230".to_string(), 60) ] ); gen_test_subscribe_candlestick!( BinanceInverseWSClient, &[ ("BTCUSD_221230".to_string(), 2592000), ("ETHUSD_221230".to_string(), 2592000), ("BNBUSD_221230".to_string(), 2592000) ] ); } } #[cfg(test)] mod binance_linear_future { use crypto_ws_client::{BinanceLinearWSClient, WSClient}; #[tokio::test(flavor = "multi_thread")] async fn subscribe() { gen_test_code!( BinanceLinearWSClient, subscribe, &[ ("aggTrade".to_string(), "BTCUSDT_221230".to_string()), ("aggTrade".to_string(), "ETHUSDT_221230".to_string()) ] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_all_bbo() { gen_test_code!( BinanceLinearWSClient, send, &[r#"{"id":9527,"method":"SUBSCRIBE","params":["!bookTicker"]}"#.to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_trade() { gen_test_code!( BinanceLinearWSClient, subscribe_trade, &["BTCUSDT_221230".to_string(), "ETHUSDT_221230".to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_ticker() { gen_test_code!( BinanceLinearWSClient, subscribe_ticker, &["BTCUSDT_221230".to_string(), "ETHUSDT_221230".to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_tickers_all() { gen_test_code!( BinanceLinearWSClient, send, &[r#"{"id":9527,"method":"SUBSCRIBE","params":["!ticker@arr"]}"#.to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_bbo() { gen_test_code!( BinanceLinearWSClient, subscribe_bbo, &["BTCUSDT_221230".to_string(), "ETHUSDT_221230".to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook() { gen_test_code!( BinanceLinearWSClient, subscribe_orderbook, &["BTCUSDT_221230".to_string(), "ETHUSDT_221230".to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook_topk() { gen_test_code!( BinanceLinearWSClient, subscribe_orderbook_topk, &["BTCUSDT_221230".to_string(), "ETHUSDT_221230".to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_candlestick() { gen_test_subscribe_candlestick!( BinanceLinearWSClient, &[("BTCUSDT_221230".to_string(), 60), ("ETHUSDT_221230".to_string(), 60)] ); gen_test_subscribe_candlestick!( BinanceLinearWSClient, &[("BTCUSDT_221230".to_string(), 2592000), ("ETHUSDT_221230".to_string(), 2592000)] ); } } #[cfg(test)] mod binance_inverse_swap { use crypto_ws_client::{BinanceInverseWSClient, WSClient}; #[tokio::test(flavor = "multi_thread")] async fn subscribe() { gen_test_code!( BinanceInverseWSClient, subscribe, &[("aggTrade".to_string(), "btcusd_perp".to_string())] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_all_bbo() { gen_test_code!( BinanceInverseWSClient, send, &[r#"{"id":9527,"method":"SUBSCRIBE","params":["!bookTicker"]}"#.to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_trade() { gen_test_code!( BinanceInverseWSClient, subscribe_trade, &["btcusd_perp".to_string(), "ethusd_perp".to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_ticker() { gen_test_code!( BinanceInverseWSClient, subscribe_ticker, &["btcusd_perp".to_string(), "ethusd_perp".to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_tickers_all() { gen_test_code!( BinanceInverseWSClient, send, &[r#"{"id":9527,"method":"SUBSCRIBE","params":["!ticker@arr"]}"#.to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_bbo() { gen_test_code!( BinanceInverseWSClient, subscribe_bbo, &["btcusd_perp".to_string(), "ethusd_perp".to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook() { gen_test_code!( BinanceInverseWSClient, subscribe_orderbook, &["btcusd_perp".to_string(), "ethusd_perp".to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook_topk() { gen_test_code!( BinanceInverseWSClient, subscribe_orderbook_topk, &["btcusd_perp".to_string(), "ethusd_perp".to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_candlestick() { gen_test_subscribe_candlestick!( BinanceInverseWSClient, &[("btcusd_perp".to_string(), 60), ("ethusd_perp".to_string(), 60)] ); gen_test_subscribe_candlestick!( BinanceInverseWSClient, &[("btcusd_perp".to_string(), 2592000), ("ethusd_perp".to_string(), 2592000)] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_funding_rate() { gen_test_code!( BinanceInverseWSClient, subscribe, &[("markPrice".to_string(), "btcusd_perp".to_string())] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_funding_rate_all() { gen_test_code!( BinanceInverseWSClient, send, &[r#"{"id":9527,"method":"SUBSCRIBE","params":["!markPrice@arr"]}"#.to_string()] ); } } #[cfg(test)] mod binance_linear_swap { use crypto_ws_client::{BinanceLinearWSClient, WSClient}; #[tokio::test(flavor = "multi_thread")] async fn subscribe() { gen_test_code!( BinanceLinearWSClient, subscribe, &[("aggTrade".to_string(), "BTCUSDT".to_string())] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_all_bbo() { gen_test_code!( BinanceLinearWSClient, send, &[r#"{"id":9527,"method":"SUBSCRIBE","params":["!bookTicker"]}"#.to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_trade() { gen_test_code!( BinanceLinearWSClient, subscribe_trade, &["BTCUSDT".to_string(), "ETHUSDT".to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_ticker() { gen_test_code!( BinanceLinearWSClient, subscribe_ticker, &["BTCUSDT".to_string(), "ETHUSDT".to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_tickers_all() { gen_test_code!( BinanceLinearWSClient, send, &[r#"{"id":9527,"method":"SUBSCRIBE","params":["!ticker@arr"]}"#.to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_bbo() { gen_test_code!( BinanceLinearWSClient, subscribe_bbo, &["BTCUSDT".to_string(), "ETHUSDT".to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook() { gen_test_code!( BinanceLinearWSClient, subscribe_orderbook, &["BTCUSDT".to_string(), "ETHUSDT".to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook_topk() { gen_test_code!( BinanceLinearWSClient, subscribe_orderbook_topk, &["BTCUSDT".to_string(), "ETHUSDT".to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_candlestick() { gen_test_subscribe_candlestick!( BinanceLinearWSClient, &[("BTCUSDT".to_string(), 60), ("ETHUSDT".to_string(), 60)] ); gen_test_subscribe_candlestick!( BinanceLinearWSClient, &[("BTCUSDT".to_string(), 2592000), ("ETHUSDT".to_string(), 2592000)] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_funding_rate() { gen_test_code!( BinanceLinearWSClient, subscribe, &[("markPrice".to_string(), "BTCUSDT".to_string())] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_funding_rate_all() { gen_test_code!( BinanceLinearWSClient, send, &[r#"{"id":9527,"method":"SUBSCRIBE","params":["!markPrice@arr"]}"#.to_string()] ); } } ================================================ FILE: crypto-ws-client/tests/binance_option.rs ================================================ use crypto_ws_client::{BinanceOptionWSClient, WSClient}; #[macro_use] mod utils; #[ignore] #[tokio::test(flavor = "multi_thread")] async fn subscribe() { gen_test_code!( BinanceOptionWSClient, subscribe, &[ ("TICKER_ALL".to_string(), "BTCUSDT".to_string()), ("TRADE_ALL".to_string(), "BTCUSDT_C".to_string()), ("TRADE_ALL".to_string(), "BTCUSDT_P".to_string()) ] ); } #[tokio::test(flavor = "multi_thread")] #[ignore] async fn subscribe_trade() { gen_test_code!( BinanceOptionWSClient, subscribe_trade, &["BTC-220325-40000-C".to_string(), "BTC-220325-35000-P".to_string()] ); } #[tokio::test(flavor = "multi_thread")] #[ignore] async fn subscribe_ticker() { gen_test_code!( BinanceOptionWSClient, subscribe_ticker, &["BTC-220325-40000-C".to_string(), "BTC-220325-35000-P".to_string()] ); } #[ignore] #[tokio::test(flavor = "multi_thread")] async fn subscribe_ticker_all() { gen_test_code!( BinanceOptionWSClient, subscribe, &[("TICKER_ALL".to_string(), "BTCUSDT".to_string())] ); } #[tokio::test(flavor = "multi_thread")] #[ignore] async fn subscribe_orderbook() { gen_test_code!( BinanceOptionWSClient, subscribe_orderbook, &["BTC-220325-40000-C".to_string(), "BTC-220325-35000-P".to_string()] ); } #[tokio::test(flavor = "multi_thread")] #[ignore] async fn subscribe_orderbook_topk() { gen_test_code!( BinanceOptionWSClient, subscribe_orderbook_topk, &["BTC-220325-40000-C".to_string(), "BTC-220325-35000-P".to_string()] ); } #[tokio::test(flavor = "multi_thread")] #[ignore] async fn subscribe_candlestick() { gen_test_subscribe_candlestick!( BinanceOptionWSClient, &[("BTC-220325-40000-C".to_string(), 60), ("BTC-220325-35000-P".to_string(), 60)] ); gen_test_subscribe_candlestick!( BinanceOptionWSClient, &[("BTC-220325-40000-C".to_string(), 60), ("BTC-220325-35000-P".to_string(), 60)] ); } ================================================ FILE: crypto-ws-client/tests/bitfinex.rs ================================================ #[macro_use] mod utils; #[cfg(test)] mod bitfinex_spot { use crypto_ws_client::{BitfinexWSClient, WSClient}; #[tokio::test(flavor = "multi_thread")] async fn subscribe() { gen_test_code!( BitfinexWSClient, subscribe, &[ ("trades".to_string(), "tBTCUST".to_string()), ("trades".to_string(), "tETHUST".to_string()) ] ); } #[test] #[should_panic] fn subscribe_illegal_symbol() { gen_test_code!( BitfinexWSClient, subscribe, &[("trades".to_string(), "tXXXYYY".to_string())] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_trade() { gen_test_code!( BitfinexWSClient, subscribe_trade, &["tBTCUST".to_string(), "tETHUST".to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_ticker() { gen_test_code!(BitfinexWSClient, subscribe_ticker, &["tBTCUST".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook() { gen_test_code!(BitfinexWSClient, subscribe_orderbook, &["tBTCUST".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_l3_orderbook() { gen_test_code!(BitfinexWSClient, subscribe_l3_orderbook, &["tBTCUST".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_candlestick() { gen_test_subscribe_candlestick!(BitfinexWSClient, &[("tBTCUST".to_string(), 60)]); gen_test_subscribe_candlestick!(BitfinexWSClient, &[("tBTCUST".to_string(), 2592000)]); } } #[cfg(test)] mod bitfinex_swap { use crypto_ws_client::{BitfinexWSClient, WSClient}; #[tokio::test(flavor = "multi_thread")] async fn subscribe() { gen_test_code!( BitfinexWSClient, subscribe, &[("trades".to_string(), "tBTCF0:USTF0".to_string())] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_trade() { gen_test_code!( BitfinexWSClient, subscribe_trade, &["tBTCF0:USTF0".to_string(), "tETHF0:USTF0".to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_ticker() { gen_test_code!(BitfinexWSClient, subscribe_ticker, &["tBTCF0:USTF0".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook() { gen_test_code!(BitfinexWSClient, subscribe_orderbook, &["tBTCF0:USTF0".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_l3_orderbook() { gen_test_code!( BitfinexWSClient, subscribe_l3_orderbook, &["tBTCF0:USTF0".to_string(), "tETHF0:USTF0".to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_candlestick() { gen_test_subscribe_candlestick!(BitfinexWSClient, &[("tBTCF0:USTF0".to_string(), 60)]); gen_test_subscribe_candlestick!(BitfinexWSClient, &[("tBTCF0:USTF0".to_string(), 2592000)]); } } ================================================ FILE: crypto-ws-client/tests/bitget.rs ================================================ #[macro_use] mod utils; #[cfg(test)] mod bitget_spot { use crypto_ws_client::{BitgetSpotWSClient, WSClient}; #[tokio::test(flavor = "multi_thread")] async fn subscribe_trade() { gen_test_code!(BitgetSpotWSClient, subscribe_trade, &["BTCUSDT".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook_topk() { gen_test_code!(BitgetSpotWSClient, subscribe_orderbook_topk, &["BTCUSDT".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook() { gen_test_code!(BitgetSpotWSClient, subscribe_orderbook, &["BTCUSDT".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_ticker() { gen_test_code!(BitgetSpotWSClient, subscribe_ticker, &["BTCUSDT".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_candlestick() { gen_test_subscribe_candlestick!(BitgetSpotWSClient, &[("BTCUSDT".to_string(), 60)]); gen_test_subscribe_candlestick!(BitgetSpotWSClient, &[("BTCUSDT".to_string(), 604800)]); } } #[cfg(test)] mod bitget_inverse_swap { use crypto_ws_client::{BitgetSwapWSClient, WSClient}; #[tokio::test(flavor = "multi_thread")] async fn subscribe() { gen_test_code!( BitgetSwapWSClient, subscribe, &[("trade".to_string(), "BTCUSD".to_string())] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_raw_json() { gen_test_code!( BitgetSwapWSClient, send, &[r#"{"op":"subscribe","args":[{"channel":"trade","instId":"BTCUSD","instType":"MC"}]}"#.to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_trade() { gen_test_code!(BitgetSwapWSClient, subscribe_trade, &["BTCUSD".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook_topk() { gen_test_code!(BitgetSwapWSClient, subscribe_orderbook_topk, &["BTCUSD".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook() { gen_test_code!(BitgetSwapWSClient, subscribe_orderbook, &["BTCUSD".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_ticker() { gen_test_code!(BitgetSwapWSClient, subscribe_ticker, &["BTCUSD".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_candlestick() { gen_test_subscribe_candlestick!(BitgetSwapWSClient, &[("BTCUSD".to_string(), 60)]); gen_test_subscribe_candlestick!(BitgetSwapWSClient, &[("BTCUSD".to_string(), 604800)]); } #[ignore] #[tokio::test(flavor = "multi_thread")] async fn subscribe_funding_rate() { gen_test_code!( BitgetSwapWSClient, subscribe, &[("funding_rate".to_string(), "BTCUSD".to_string())] ); } } #[cfg(test)] mod bitget_linear_swap { use crypto_ws_client::{BitgetSwapWSClient, WSClient}; #[tokio::test(flavor = "multi_thread")] async fn subscribe_trade() { gen_test_code!(BitgetSwapWSClient, subscribe_trade, &["BTCUSDT".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook_topk() { gen_test_code!(BitgetSwapWSClient, subscribe_orderbook_topk, &["BTCUSDT".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook() { gen_test_code!(BitgetSwapWSClient, subscribe_orderbook, &["BTCUSDT".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_ticker() { gen_test_code!(BitgetSwapWSClient, subscribe_ticker, &["BTCUSDT".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_candlestick() { gen_test_subscribe_candlestick!(BitgetSwapWSClient, &[("BTCUSDT".to_string(), 60)]); gen_test_subscribe_candlestick!(BitgetSwapWSClient, &[("BTCUSDT".to_string(), 604800)]); } #[ignore] #[tokio::test(flavor = "multi_thread")] async fn subscribe_funding_rate() { gen_test_code!( BitgetSwapWSClient, subscribe, &[("funding_rate".to_string(), "BTCUSDT".to_string())] ); } } ================================================ FILE: crypto-ws-client/tests/bithumb.rs ================================================ use crypto_ws_client::{BithumbWSClient, WSClient}; #[macro_use] mod utils; #[ignore = "duplicated"] #[tokio::test(flavor = "multi_thread")] async fn subscribe() { gen_test_code!( BithumbWSClient, subscribe, &[ ("TRADE".to_string(), "BTC-USDT".to_string()), ("TRADE".to_string(), "ETH-USDT".to_string()) ] ); } #[test] #[should_panic] fn subscribe_illegal_symbol() { gen_test_code!(BithumbWSClient, subscribe, &[("TRADE".to_string(), "XXX-YYY".to_string())]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_raw_json() { gen_test_code!( BithumbWSClient, send, &[r#"{"cmd":"subscribe","args":["TRADE:BTC-USDT","TRADE:ETH-USDT"]}"#.to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_trade() { gen_test_code!( BithumbWSClient, subscribe_trade, &["BTC-USDT".to_string(), "ETH-USDT".to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook() { gen_test_code!( BithumbWSClient, subscribe_orderbook, &["BTC-USDT".to_string(), "ETH-USDT".to_string()] ); } #[ignore = "too slow"] #[tokio::test(flavor = "multi_thread")] async fn subscribe_ticker() { gen_test_code!( BithumbWSClient, subscribe_ticker, &["BTC-USDT".to_string(), "ETH-USDT".to_string()] ); } ================================================ FILE: crypto-ws-client/tests/bitmex.rs ================================================ use crypto_ws_client::{BitmexWSClient, WSClient}; #[macro_use] mod utils; #[tokio::test(flavor = "multi_thread")] async fn bitmex_instrument() { gen_test_code!( BitmexWSClient, send, &[r#"{"op":"subscribe","args":["instrument"]}"#.to_string()] ); } #[cfg(test)] mod bitmex_inverse_swap { use crypto_ws_client::{BitmexWSClient, WSClient}; #[tokio::test(flavor = "multi_thread")] async fn subscribe() { gen_test_code!( BitmexWSClient, subscribe, &[ ("trade".to_string(), "XBTUSD".to_string()), ("quote".to_string(), "XBTUSD".to_string()) ] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_raw_json() { gen_test_code!( BitmexWSClient, send, &[r#"{"op":"subscribe","args":["trade:XBTUSD","quote:XBTUSD"]}"#.to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_trade() { gen_test_code!(BitmexWSClient, subscribe_trade, &["XBTUSD".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_bbo() { gen_test_code!(BitmexWSClient, subscribe_bbo, &["XBTUSD".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook() { gen_test_code!(BitmexWSClient, subscribe_orderbook, &["XBTUSD".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook_topk() { gen_test_code!(BitmexWSClient, subscribe_orderbook_topk, &["XBTUSD".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_candlestick() { gen_test_subscribe_candlestick!(BitmexWSClient, &[("XBTUSD".to_string(), 60)]); gen_test_subscribe_candlestick!(BitmexWSClient, &[("XBTUSD".to_string(), 86400)]); } #[test] #[ignore] fn subscribe_funding_rate() { gen_test_code!(BitmexWSClient, subscribe, &[("funding".to_string(), "XBTUSD".to_string())]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_funding_rate_all() { gen_test_code!( BitmexWSClient, send, &[r#"{"op":"subscribe","args":["funding"]}"#.to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_instrument() { gen_test_code!( BitmexWSClient, subscribe, &[("instrument".to_string(), "XBTUSD".to_string())] ); } } #[cfg(test)] mod bitmex_inverse_future { use crypto_ws_client::{BitmexWSClient, WSClient}; #[tokio::test(flavor = "multi_thread")] async fn subscribe() { gen_test_code!( BitmexWSClient, subscribe, &[ ("trade".to_string(), "XBTZ22".to_string()), ("quote".to_string(), "XBTZ22".to_string()) ] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_trade() { gen_test_code!( BitmexWSClient, subscribe_trade, &["XBTZ22".to_string(), "XBTZ22".to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_bbo() { gen_test_code!( BitmexWSClient, subscribe_bbo, &["XBTZ22".to_string(), "XBTZ22".to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook() { gen_test_code!( BitmexWSClient, subscribe_orderbook, &["XBTZ22".to_string(), "XBTZ22".to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook_topk() { gen_test_code!( BitmexWSClient, subscribe_orderbook_topk, &["XBTZ22".to_string(), "XBTZ22".to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_candlestick() { gen_test_subscribe_candlestick!( BitmexWSClient, &[("XBTZ22".to_string(), 60), ("XBTZ22".to_string(), 60)] ); gen_test_subscribe_candlestick!( BitmexWSClient, &[("XBTZ22".to_string(), 86400), ("XBTZ22".to_string(), 86400)] ); } } #[cfg(test)] mod bitmex_quanto_swap { use crypto_ws_client::{BitmexWSClient, WSClient}; #[tokio::test(flavor = "multi_thread")] async fn subscribe_trade() { gen_test_code!(BitmexWSClient, subscribe_trade, &["ETHUSD".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_bbo() { gen_test_code!(BitmexWSClient, subscribe_bbo, &["ETHUSD".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook() { gen_test_code!(BitmexWSClient, subscribe_orderbook, &["ETHUSD".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook_topk() { gen_test_code!(BitmexWSClient, subscribe_orderbook_topk, &["ETHUSD".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_candlestick() { gen_test_subscribe_candlestick!(BitmexWSClient, &[("ETHUSD".to_string(), 60)]); gen_test_subscribe_candlestick!(BitmexWSClient, &[("ETHUSD".to_string(), 86400)]); } #[test] #[ignore] fn subscribe_funding_rate() { gen_test_code!(BitmexWSClient, subscribe, &[("funding".to_string(), "ETHUSD".to_string())]); } } #[cfg(test)] mod bitmex_linear_future { use crypto_ws_client::{BitmexWSClient, WSClient}; #[tokio::test(flavor = "multi_thread")] async fn subscribe_trade() { gen_test_code!( BitmexWSClient, subscribe_trade, &["XBTUSDTZ22".to_string(), "ETHZ22".to_string(), "ETHUSDTZ22".to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_bbo() { gen_test_code!(BitmexWSClient, subscribe_bbo, &["ETHZ22".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook() { gen_test_code!(BitmexWSClient, subscribe_orderbook, &["ETHZ22".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook_topk() { gen_test_code!(BitmexWSClient, subscribe_orderbook_topk, &["ETHZ22".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_candlestick() { gen_test_subscribe_candlestick!(BitmexWSClient, &[("ETHZ22".to_string(), 60)]); gen_test_subscribe_candlestick!(BitmexWSClient, &[("ETHZ22".to_string(), 86400)]); } } ================================================ FILE: crypto-ws-client/tests/bitstamp.rs ================================================ use crypto_ws_client::{BitstampWSClient, WSClient}; #[macro_use] mod utils; #[tokio::test(flavor = "multi_thread")] async fn subscribe() { gen_test_code!( BitstampWSClient, subscribe, &[ ("live_trades".to_string(), "btcusd".to_string()), ("diff_order_book".to_string(), "btcusd".to_string()) ] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_raw_json() { gen_test_code!( BitstampWSClient, send, &[ r#"{"event":"bts:subscribe","data":{"channel":"live_trades_btcusd"}}"#.to_string(), r#"{"event":"bts:subscribe","data":{"channel":"live_trades_ethusd"}}"#.to_string() ] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_trade() { gen_test_code!( BitstampWSClient, subscribe_trade, &["btcusd".to_string(), "ethusd".to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook() { gen_test_code!( BitstampWSClient, subscribe_orderbook, &["btcusd".to_string(), "ethusd".to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook_topk() { gen_test_code!( BitstampWSClient, subscribe_orderbook_topk, &["btcusd".to_string(), "ethusd".to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_l3_orderbook() { gen_test_code!( BitstampWSClient, subscribe_l3_orderbook, &["btcusd".to_string(), "ethusd".to_string()] ); } ================================================ FILE: crypto-ws-client/tests/bitz.rs ================================================ #[macro_use] mod utils; #[cfg(test)] mod bitz_spot { use crypto_ws_client::{BitzSpotWSClient, WSClient}; use std::time::{SystemTime, UNIX_EPOCH}; #[tokio::test(flavor = "multi_thread")] #[ignore = "bitz.com has shutdown since October 2021"] async fn subscribe() { gen_test_code!( BitzSpotWSClient, subscribe, &[ ("market".to_string(), "btc_usdt".to_string()), ("depth".to_string(), "btc_usdt".to_string()), ("order".to_string(), "btc_usdt".to_string()) ] ); } #[tokio::test(flavor = "multi_thread")] #[ignore = "bitz.com has shutdown since October 2021"] async fn subscribe_raw_json() { gen_test_code!( BitzSpotWSClient, send, &[format!( r#"{{"action":"Topic.sub", "data":{{"symbol":"btc_usdt", "type":"market,depth,order", "_CDID":"100002", "dataType":"1"}}, "msg_id":{}}}"#, SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() )] ); } #[tokio::test(flavor = "multi_thread")] #[ignore = "bitz.com has shutdown since October 2021"] async fn subscribe_trade() { gen_test_code!(BitzSpotWSClient, subscribe_trade, &["btc_usdt".to_string()]); } #[tokio::test(flavor = "multi_thread")] #[ignore = "bitz.com has shutdown since October 2021"] async fn subscribe_orderbook() { gen_test_code!(BitzSpotWSClient, subscribe_orderbook, &["btc_usdt".to_string()]); } #[tokio::test(flavor = "multi_thread")] #[ignore = "bitz.com has shutdown since October 2021"] async fn subscribe_ticker() { gen_test_code!(BitzSpotWSClient, subscribe_ticker, &["btc_usdt".to_string()]); } #[tokio::test(flavor = "multi_thread")] #[ignore = "bitz.com has shutdown since October 2021"] async fn subscribe_candlestick() { gen_test_subscribe_candlestick!(BitzSpotWSClient, &[("btc_usdt".to_string(), 60)]); gen_test_subscribe_candlestick!(BitzSpotWSClient, &[("btc_usdt".to_string(), 2592000)]); } } ================================================ FILE: crypto-ws-client/tests/bybit.rs ================================================ #[macro_use] mod utils; #[cfg(test)] mod bybit_inverse_future { use crypto_ws_client::{BybitInverseWSClient, WSClient}; #[tokio::test(flavor = "multi_thread")] async fn subscribe() { gen_test_code!( BybitInverseWSClient, subscribe, &[("trade".to_string(), "BTCUSDZ22".to_string())] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_raw_json() { gen_test_code!( BybitInverseWSClient, send, &[r#"{"op":"subscribe","args":["trade.BTCUSDZ22"]}"#.to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_trade() { gen_test_code!(BybitInverseWSClient, subscribe_trade, &["BTCUSDZ22".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook() { gen_test_code!(BybitInverseWSClient, subscribe_orderbook, &["BTCUSDZ22".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_ticker() { gen_test_code!(BybitInverseWSClient, subscribe_ticker, &["BTCUSDZ22".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_candlestick() { gen_test_subscribe_candlestick!(BybitInverseWSClient, &[("BTCUSDZ22".to_string(), 60)]); gen_test_subscribe_candlestick!( BybitInverseWSClient, &[("BTCUSDZ22".to_string(), 2592000)] ); } } #[cfg(test)] mod bybit_inverse_swap { use crypto_ws_client::{BybitInverseWSClient, WSClient}; #[tokio::test(flavor = "multi_thread")] async fn subscribe() { gen_test_code!( BybitInverseWSClient, subscribe, &[("trade".to_string(), "BTCUSD".to_string())] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_raw_json() { gen_test_code!( BybitInverseWSClient, send, &[r#"{"op":"subscribe","args":["trade.BTCUSD"]}"#.to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_trade() { gen_test_code!(BybitInverseWSClient, subscribe_trade, &["BTCUSD".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook() { gen_test_code!(BybitInverseWSClient, subscribe_orderbook, &["BTCUSD".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_ticker() { gen_test_code!(BybitInverseWSClient, subscribe_ticker, &["BTCUSD".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_candlestick() { gen_test_subscribe_candlestick!(BybitInverseWSClient, &[("BTCUSD".to_string(), 60)]); gen_test_subscribe_candlestick!(BybitInverseWSClient, &[("BTCUSD".to_string(), 2592000)]); } } #[cfg(test)] mod bybit_linear_swap { use crypto_ws_client::{BybitLinearSwapWSClient, WSClient}; #[tokio::test(flavor = "multi_thread")] async fn subscribe_trade() { gen_test_code!(BybitLinearSwapWSClient, subscribe_trade, &["BTCUSDT".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook() { gen_test_code!(BybitLinearSwapWSClient, subscribe_orderbook, &["BTCUSDT".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_ticker() { gen_test_code!(BybitLinearSwapWSClient, subscribe_ticker, &["BTCUSDT".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_candlestick() { gen_test_subscribe_candlestick!(BybitLinearSwapWSClient, &[("BTCUSDT".to_string(), 60)]); gen_test_subscribe_candlestick!( BybitLinearSwapWSClient, &[("BTCUSDT".to_string(), 2592000)] ); } } ================================================ FILE: crypto-ws-client/tests/coinbase_pro.rs ================================================ use crypto_ws_client::{CoinbaseProWSClient, WSClient}; #[macro_use] mod utils; #[tokio::test(flavor = "multi_thread")] async fn subscribe() { gen_test_code!( CoinbaseProWSClient, subscribe, &[ ("matches".to_string(), "BTC-USD".to_string()), ("heartbeat".to_string(), "BTC-USD".to_string()) ] ); } #[tokio::test(flavor = "multi_thread")] #[should_panic] async fn subscribe_illegal_symbol() { gen_test_code!( CoinbaseProWSClient, subscribe, &[("matches".to_string(), "XXX-YYY".to_string())] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_raw_json() { gen_test_code!( CoinbaseProWSClient, send, &[r#"{ "type":"subscribe", "channels":[ { "name":"heartbeat", "product_ids":[ "BTC-USD" ] }, { "name":"matches", "product_ids":[ "BTC-USD" ] } ] }"# .to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_trade() { gen_test_code!( CoinbaseProWSClient, subscribe_trade, &["BTC-USD".to_string(), "ETH-USD".to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_ticker() { gen_test_code!( CoinbaseProWSClient, subscribe_ticker, &["BTC-USD".to_string(), "ETH-USD".to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook() { gen_test_code!(CoinbaseProWSClient, subscribe_orderbook, &["BTC-USD".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_l3_orderbook() { gen_test_code!(CoinbaseProWSClient, subscribe_l3_orderbook, &["BTC-USD".to_string()]); } ================================================ FILE: crypto-ws-client/tests/deribit.rs ================================================ use crypto_ws_client::{DeribitWSClient, WSClient}; #[macro_use] mod utils; #[tokio::test(flavor = "multi_thread")] async fn deribit_all_trades() { gen_test_code!( DeribitWSClient, subscribe, // https://docs.deribit.com/?javascript#trades-kind-currency-interval &[ ("trades.future.SYMBOL.100ms".to_string(), "any".to_string()), ("trades.option.SYMBOL.100ms".to_string(), "any".to_string()) ] ); } #[cfg(test)] mod deribit_inverse_future { use crypto_ws_client::{DeribitWSClient, WSClient}; #[ignore = "lack of liquidity"] #[tokio::test(flavor = "multi_thread")] async fn subscribe() { gen_test_code!( DeribitWSClient, subscribe, &[("trades.future.SYMBOL.100ms".to_string(), "BTC".to_string())] ); } #[ignore = "lack of liquidity"] #[tokio::test(flavor = "multi_thread")] async fn subscribe_trade() { gen_test_code!( DeribitWSClient, subscribe_trade, &["BTC-26AUG22".to_string(), "BTC-30DEC22".to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_ticker() { gen_test_code!(DeribitWSClient, subscribe_ticker, &["BTC-30DEC22".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook() { gen_test_code!(DeribitWSClient, subscribe_orderbook, &["BTC-30DEC22".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook_topk() { gen_test_code!(DeribitWSClient, subscribe_orderbook_topk, &["BTC-30DEC22".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_bbo() { gen_test_code!(DeribitWSClient, subscribe_bbo, &["BTC-30DEC22".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_candlestick() { gen_test_subscribe_candlestick!(DeribitWSClient, &[("BTC-30DEC22".to_string(), 60)]); gen_test_subscribe_candlestick!(DeribitWSClient, &[("BTC-30DEC22".to_string(), 86400)]); } } #[cfg(test)] mod deribit_inverse_swap { use crypto_ws_client::{DeribitWSClient, WSClient}; #[tokio::test(flavor = "multi_thread")] async fn subscribe() { gen_test_code!( DeribitWSClient, subscribe, &[("trades.SYMBOL.100ms".to_string(), "BTC-PERPETUAL".to_string())] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_trade() { gen_test_code!(DeribitWSClient, subscribe_trade, &["BTC-PERPETUAL".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_ticker() { gen_test_code!(DeribitWSClient, subscribe_ticker, &["BTC-PERPETUAL".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook() { gen_test_code!(DeribitWSClient, subscribe_orderbook, &["BTC-PERPETUAL".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook_topk() { gen_test_code!(DeribitWSClient, subscribe_orderbook_topk, &["BTC-PERPETUAL".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_bbo() { gen_test_code!(DeribitWSClient, subscribe_bbo, &["BTC-PERPETUAL".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_candlestick() { gen_test_subscribe_candlestick!(DeribitWSClient, &[("BTC-PERPETUAL".to_string(), 60)]); gen_test_subscribe_candlestick!(DeribitWSClient, &[("BTC-PERPETUAL".to_string(), 86400)]); } } #[cfg(test)] mod deribit_option { use crypto_ws_client::{DeribitWSClient, WSClient}; const SYMBOLS: &[&str] = &[ "BTC-26AUG22-23000-C", "BTC-26AUG22-45000-C", "BTC-30DEC22-40000-C", "BTC-30DEC22-60000-C", ]; #[ignore = "lack of liquidity"] #[tokio::test(flavor = "multi_thread")] async fn subscribe() { gen_test_code!( DeribitWSClient, subscribe, &[("trades.option.SYMBOL.100ms".to_string(), "any".to_string())] ); } #[tokio::test(flavor = "multi_thread")] #[ignore] async fn subscribe_trade() { gen_test_code!( DeribitWSClient, subscribe_trade, &SYMBOLS.iter().map(|s| s.to_string()).collect::>() ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_ticker() { gen_test_code!( DeribitWSClient, subscribe_ticker, &SYMBOLS.iter().map(|s| s.to_string()).collect::>() ); } #[tokio::test(flavor = "multi_thread")] #[ignore] async fn subscribe_orderbook() { gen_test_code!( DeribitWSClient, subscribe_orderbook, &SYMBOLS.iter().map(|s| s.to_string()).collect::>() ); } #[tokio::test(flavor = "multi_thread")] #[ignore] async fn subscribe_orderbook_topk() { gen_test_code!( DeribitWSClient, subscribe_orderbook_topk, &SYMBOLS.iter().map(|s| s.to_string()).collect::>() ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_bbo() { gen_test_code!( DeribitWSClient, subscribe_bbo, &SYMBOLS.iter().map(|s| s.to_string()).collect::>() ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_candlestick() { gen_test_subscribe_candlestick!( DeribitWSClient, SYMBOLS .iter() .map(|s| (s.to_string(), 60)) .collect::>() .as_slice() ); gen_test_subscribe_candlestick!( DeribitWSClient, SYMBOLS .iter() .map(|s| (s.to_string(), 86400)) .collect::>() .as_slice() ); } } ================================================ FILE: crypto-ws-client/tests/dydx.rs ================================================ #[macro_use] mod utils; #[cfg(test)] mod dydx_linear_swap { use crypto_ws_client::{DydxSwapWSClient, WSClient}; #[tokio::test(flavor = "multi_thread")] async fn subscribe() { gen_test_code!( DydxSwapWSClient, send, &[ r#"{"type": "subscribe", "channel": "v3_trades", "id": "BTC-USD"}"#.to_string(), r#"{"type": "subscribe", "channel": "v3_trades", "id": "ETH-USD"}"#.to_string() ] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_trade() { gen_test_code!( DydxSwapWSClient, subscribe_trade, &["BTC-USD".to_string(), "ETH-USD".to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook() { gen_test_code!( DydxSwapWSClient, subscribe_orderbook, &["BTC-USD".to_string(), "ETH-USD".to_string()] ); } } ================================================ FILE: crypto-ws-client/tests/ftx.rs ================================================ #[macro_use] mod utils; #[cfg(test)] mod ftx_spot { use crypto_ws_client::{FtxWSClient, WSClient}; #[tokio::test(flavor = "multi_thread")] async fn subscribe() { gen_test_code!(FtxWSClient, subscribe, &[("trades".to_string(), "BTC/USD".to_string())]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_raw_json() { gen_test_code!( FtxWSClient, send, &[r#"{"op":"subscribe","channel":"trades","market":"BTC/USD"}"#.to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_trade() { gen_test_code!(FtxWSClient, subscribe_trade, &["BTC/USD".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_bbo() { gen_test_code!(FtxWSClient, subscribe_bbo, &["BTC/USD".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook() { gen_test_code!(FtxWSClient, subscribe_orderbook, &["BTC/USD".to_string()]); } } #[cfg(test)] mod ftx_linear_swap { use crypto_ws_client::{FtxWSClient, WSClient}; #[tokio::test(flavor = "multi_thread")] async fn subscribe_trade() { gen_test_code!(FtxWSClient, subscribe_trade, &["BTC-PERP".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_bbo() { gen_test_code!(FtxWSClient, subscribe_bbo, &["BTC-PERP".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook() { gen_test_code!( FtxWSClient, subscribe_orderbook, &["BTC-PERP".to_string(), "ETH-PERP".to_string()] ); } } #[cfg(test)] mod ftx_linear_future { use crypto_ws_client::{FtxWSClient, WSClient}; #[tokio::test(flavor = "multi_thread")] async fn subscribe_trade() { gen_test_code!( FtxWSClient, subscribe_trade, &["BTC-1230".to_string(), "ETH-1230".to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_bbo() { gen_test_code!( FtxWSClient, subscribe_bbo, &["BTC-1230".to_string(), "ETH-1230".to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook() { gen_test_code!(FtxWSClient, subscribe_orderbook, &["BTC-1230".to_string()]); } } #[cfg(test)] mod ftx_move { use crypto_ws_client::{FtxWSClient, WSClient}; #[test] #[ignore] fn subscribe_trade() { gen_test_code!(FtxWSClient, subscribe_trade, &["BTC-MOVE-2022Q4".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_bbo() { gen_test_code!(FtxWSClient, subscribe_bbo, &["BTC-MOVE-2022Q4".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook() { gen_test_code!(FtxWSClient, subscribe_orderbook, &["BTC-MOVE-2022Q4".to_string()]); } } #[cfg(test)] mod ftx_bvol { use crypto_ws_client::{FtxWSClient, WSClient}; #[test] #[ignore] fn subscribe_trade() { gen_test_code!(FtxWSClient, subscribe_trade, &["BVOL/USD".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_bbo() { gen_test_code!(FtxWSClient, subscribe_bbo, &["BVOL/USD".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook() { gen_test_code!(FtxWSClient, subscribe_orderbook, &["BVOL/USD".to_string()]); } } ================================================ FILE: crypto-ws-client/tests/gate.rs ================================================ #[macro_use] mod utils; #[cfg(test)] mod gate_spot { use crypto_ws_client::{GateSpotWSClient, WSClient}; #[tokio::test(flavor = "multi_thread")] async fn subscribe() { gen_test_code!( GateSpotWSClient, subscribe, &[ ("trades".to_string(), "BTC_USDT".to_string()), ("trades".to_string(), "ETH_USDT".to_string()) ] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_raw_json() { gen_test_code!( GateSpotWSClient, send, &[r#"{"channel":"spot.trades", "event":"subscribe", "payload":["BTC_USDT","ETH_USDT"]}"#.to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_trade() { gen_test_code!(GateSpotWSClient, subscribe_trade, &["BTC_USDT".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook() { gen_test_code!(GateSpotWSClient, subscribe_orderbook, &["BTC_USDT".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook_topk() { gen_test_code!(GateSpotWSClient, subscribe_orderbook_topk, &["BTC_USDT".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_bbo() { gen_test_code!(GateSpotWSClient, subscribe_bbo, &["BTC_USDT".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_ticker() { gen_test_code!(GateSpotWSClient, subscribe_ticker, &["BTC_USDT".to_string()]); } #[ignore = "too slow"] #[tokio::test(flavor = "multi_thread")] async fn subscribe_candlestick() { gen_test_subscribe_candlestick!(GateSpotWSClient, &[("BTC_USDT".to_string(), 10)]); gen_test_subscribe_candlestick!(GateSpotWSClient, &[("BTC_USDT".to_string(), 604800)]); } } #[cfg(test)] mod gate_inverse_swap { use crypto_ws_client::{GateInverseSwapWSClient, WSClient}; #[ignore = "lack of liquidity"] #[tokio::test(flavor = "multi_thread")] async fn subscribe() { gen_test_code!( GateInverseSwapWSClient, subscribe, &[ ("trades".to_string(), "BTC_USD".to_string()), ("trades".to_string(), "ETH_USD".to_string()), ("trades".to_string(), "BNB_USD".to_string()), ("trades".to_string(), "XRP_USD".to_string()) ] ); } #[ignore = "lack of liquidity"] #[tokio::test(flavor = "multi_thread")] async fn subscribe_raw_json() { gen_test_code!( GateInverseSwapWSClient, send, &[r#"{"channel":"futures.trades", "event":"subscribe", "payload":["BTC_USD","ETH_USD","BNB_USD","XRP_USD"]}"# .to_string()] ); } #[ignore = "lack of liquidity"] #[tokio::test(flavor = "multi_thread")] async fn subscribe_trade() { gen_test_code!( GateInverseSwapWSClient, subscribe_trade, &["BTC_USD".to_string(), "ETH_USD".to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook() { gen_test_code!(GateInverseSwapWSClient, subscribe_orderbook, &["BTC_USD".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook_topk() { gen_test_code!(GateInverseSwapWSClient, subscribe_orderbook_topk, &["BTC_USD".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_bbo() { gen_test_code!(GateInverseSwapWSClient, subscribe_bbo, &["BTC_USD".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_ticker() { gen_test_code!(GateInverseSwapWSClient, subscribe_ticker, &["BTC_USD".to_string()]); } #[ignore] #[tokio::test(flavor = "multi_thread")] async fn subscribe_candlestick() { gen_test_subscribe_candlestick!(GateInverseSwapWSClient, &[("BTC_USD".to_string(), 10)]); gen_test_subscribe_candlestick!( GateInverseSwapWSClient, &[("BTC_USD".to_string(), 604800)] ); } } #[cfg(test)] mod gate_linear_swap { use crypto_ws_client::{GateLinearSwapWSClient, WSClient}; #[tokio::test(flavor = "multi_thread")] async fn subscribe_trade() { gen_test_code!(GateLinearSwapWSClient, subscribe_trade, &["BTC_USDT".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook() { gen_test_code!(GateLinearSwapWSClient, subscribe_orderbook, &["BTC_USDT".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook_topk() { gen_test_code!(GateLinearSwapWSClient, subscribe_orderbook_topk, &["BTC_USDT".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_bbo() { gen_test_code!(GateLinearSwapWSClient, subscribe_bbo, &["BTC_USDT".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_ticker() { gen_test_code!(GateLinearSwapWSClient, subscribe_ticker, &["BTC_USDT".to_string()]); } #[ignore] #[tokio::test(flavor = "multi_thread")] async fn subscribe_candlestick() { gen_test_subscribe_candlestick!(GateLinearSwapWSClient, &[("BTC_USDT".to_string(), 10)]); gen_test_subscribe_candlestick!( GateLinearSwapWSClient, &[("BTC_USDT".to_string(), 604800)] ); } } #[cfg(test)] mod gate_inverse_future { use crypto_ws_client::{GateInverseFutureWSClient, WSClient}; #[test] #[ignore] fn subscribe_trade() { gen_test_code!( GateInverseFutureWSClient, subscribe_trade, &["BTC_USD_20221230".to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook() { gen_test_code!( GateInverseFutureWSClient, subscribe_orderbook, &["BTC_USD_20221230".to_string()] ); } #[ignore = "lack of liquidity"] #[tokio::test(flavor = "multi_thread")] async fn subscribe_ticker() { gen_test_code!( GateInverseFutureWSClient, subscribe_ticker, &["BTC_USD_20221230".to_string(),] ); } #[ignore] #[tokio::test(flavor = "multi_thread")] async fn subscribe_candlestick() { gen_test_subscribe_candlestick!( GateInverseFutureWSClient, &[("BTC_USD_20221230".to_string(), 10)] ); gen_test_subscribe_candlestick!( GateInverseFutureWSClient, &[("BTC_USD_20221230".to_string(), 604800)] ); } } #[cfg(test)] mod gate_linear_future { use crypto_ws_client::{GateLinearFutureWSClient, WSClient}; #[test] #[ignore] fn subscribe_trade() { gen_test_code!( GateLinearFutureWSClient, subscribe_trade, &["BTC_USDT_20221230".to_string()] ); } #[test] #[ignore] fn subscribe_orderbook() { gen_test_code!( GateLinearFutureWSClient, subscribe_orderbook, &["BTC_USDT_20221230".to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_ticker() { gen_test_code!( GateLinearFutureWSClient, subscribe_ticker, &["BTC_USDT_20221230".to_string()] ); } #[ignore] #[tokio::test(flavor = "multi_thread")] async fn subscribe_candlestick() { gen_test_subscribe_candlestick!( GateLinearFutureWSClient, &[("BTC_USDT_20221230".to_string(), 10)] ); gen_test_subscribe_candlestick!( GateLinearFutureWSClient, &[("BTC_USDT_20221230".to_string(), 604800)] ); } } ================================================ FILE: crypto-ws-client/tests/huobi.rs ================================================ #[macro_use] mod utils; #[cfg(test)] mod huobi_spot { use crypto_ws_client::{HuobiSpotWSClient, WSClient}; #[tokio::test(flavor = "multi_thread")] async fn subscribe() { gen_test_code!( HuobiSpotWSClient, subscribe, &[("trade.detail".to_string(), "btcusdt".to_string())] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_raw_json() { gen_test_code!( HuobiSpotWSClient, send, &[r#"{"sub":"market.btcusdt.trade.detail","id":"crypto-ws-client"}"#.to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_trade() { gen_test_code!(HuobiSpotWSClient, subscribe_trade, &["btcusdt".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_ticker() { gen_test_code!(HuobiSpotWSClient, subscribe_ticker, &["btcusdt".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_bbo() { gen_test_code!(HuobiSpotWSClient, subscribe_bbo, &["btcusdt".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook() { let (tx, rx) = std::sync::mpsc::channel(); tokio::task::spawn(async move { let ws_client = HuobiSpotWSClient::new(tx, Some("wss://api.huobi.pro/feed")).await; ws_client.subscribe_orderbook(&["btcusdt".to_string()]).await; // run for 60 seconds at most let _ = tokio::time::timeout(std::time::Duration::from_secs(60), ws_client.run()).await; ws_client.close().await; }); rx.into_iter().next().expect("should has at least 1 element"); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook_topk() { gen_test_code!(HuobiSpotWSClient, subscribe_orderbook_topk, &["btcusdt".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_candlestick() { gen_test_subscribe_candlestick!(HuobiSpotWSClient, &[("btcusdt".to_string(), 60)]); gen_test_subscribe_candlestick!(HuobiSpotWSClient, &[("btcusdt".to_string(), 2592000)]); } } #[cfg(test)] mod huobi_inverse_future { use crypto_ws_client::{HuobiFutureWSClient, WSClient}; #[tokio::test(flavor = "multi_thread")] async fn subscribe() { gen_test_code!( HuobiFutureWSClient, subscribe, &[("trade.detail".to_string(), "BTC_CQ".to_string())] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_trade() { gen_test_code!(HuobiFutureWSClient, subscribe_trade, &["BTC_CQ".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_ticker() { gen_test_code!(HuobiFutureWSClient, subscribe_ticker, &["BTC_CQ".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_bbo() { gen_test_code!(HuobiFutureWSClient, subscribe_bbo, &["BTC_CQ".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook() { gen_test_code!(HuobiFutureWSClient, subscribe_orderbook, &["BTC_CQ".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook_topk() { gen_test_code!(HuobiFutureWSClient, subscribe_orderbook_topk, &["BTC_CQ".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_candlestick() { gen_test_subscribe_candlestick!(HuobiFutureWSClient, &[("BTC_CQ".to_string(), 60)]); gen_test_subscribe_candlestick!(HuobiFutureWSClient, &[("BTC_CQ".to_string(), 2592000)]); } } #[cfg(test)] mod huobi_linear_swap { use crypto_ws_client::{HuobiLinearSwapWSClient, WSClient}; #[tokio::test(flavor = "multi_thread")] async fn subscribe() { gen_test_code!( HuobiLinearSwapWSClient, subscribe, &[("trade.detail".to_string(), "BTC-USDT".to_string())] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_trade() { gen_test_code!(HuobiLinearSwapWSClient, subscribe_trade, &["BTC-USDT".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_ticker() { gen_test_code!(HuobiLinearSwapWSClient, subscribe_ticker, &["BTC-USDT".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_bbo() { gen_test_code!(HuobiLinearSwapWSClient, subscribe_bbo, &["BTC-USDT".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook() { gen_test_code!(HuobiLinearSwapWSClient, subscribe_orderbook, &["BTC-USDT".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook_topk() { gen_test_code!( HuobiLinearSwapWSClient, subscribe_orderbook_topk, &["BTC-USDT".to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_candlestick() { gen_test_subscribe_candlestick!(HuobiLinearSwapWSClient, &[("BTC-USDT".to_string(), 60)]); gen_test_subscribe_candlestick!( HuobiLinearSwapWSClient, &[("BTC-USDT".to_string(), 2592000)] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_funding_rate() { let (tx, rx) = std::sync::mpsc::channel(); tokio::task::spawn(async move { let ws_client = HuobiLinearSwapWSClient::new( tx, Some("wss://api.hbdm.com/linear-swap-notification"), ) .await; ws_client .send(&[r#"{"topic":"public.BTC-USDT.funding_rate","op":"sub"}"#.to_string()]) .await; // run for 60 seconds at most let _ = tokio::time::timeout(std::time::Duration::from_secs(60), ws_client.run()).await; ws_client.close().await; }); rx.into_iter().next().expect("should has at least 1 element"); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_funding_rate_all() { let (tx, rx) = std::sync::mpsc::channel(); tokio::task::spawn(async move { let ws_client = HuobiLinearSwapWSClient::new( tx, Some("wss://api.hbdm.com/linear-swap-notification"), ) .await; ws_client.send(&[r#"{"topic":"public.*.funding_rate","op":"sub"}"#.to_string()]).await; // run for 60 seconds at most let _ = tokio::time::timeout(std::time::Duration::from_secs(60), ws_client.run()).await; ws_client.close().await; }); rx.into_iter().next().expect("should has at least 1 element"); } } #[cfg(test)] mod huobi_inverse_swap { use crypto_ws_client::{HuobiInverseSwapWSClient, WSClient}; #[tokio::test(flavor = "multi_thread")] async fn subscribe() { gen_test_code!( HuobiInverseSwapWSClient, subscribe, &[("trade.detail".to_string(), "BTC-USD".to_string())] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_trade() { gen_test_code!(HuobiInverseSwapWSClient, subscribe_trade, &["BTC-USD".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_ticker() { gen_test_code!(HuobiInverseSwapWSClient, subscribe_ticker, &["BTC-USD".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_bbo() { gen_test_code!(HuobiInverseSwapWSClient, subscribe_bbo, &["BTC-USD".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook() { gen_test_code!(HuobiInverseSwapWSClient, subscribe_orderbook, &["BTC-USD".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook_topk() { gen_test_code!( HuobiInverseSwapWSClient, subscribe_orderbook_topk, &["BTC-USD".to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_candlestick() { gen_test_subscribe_candlestick!(HuobiInverseSwapWSClient, &[("BTC-USD".to_string(), 60)]); gen_test_subscribe_candlestick!( HuobiInverseSwapWSClient, &[("BTC-USD".to_string(), 2592000)] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_funding_rate() { let (tx, rx) = std::sync::mpsc::channel(); tokio::task::spawn(async move { let ws_client = HuobiInverseSwapWSClient::new(tx, Some("wss://api.hbdm.com/swap-notification")) .await; ws_client .send(&[r#"{"topic":"public.BTC-USD.funding_rate","op":"sub"}"#.to_string()]) .await; // run for 60 seconds at most let _ = tokio::time::timeout(std::time::Duration::from_secs(60), ws_client.run()).await; ws_client.close().await; }); rx.into_iter().next().expect("should has at least 1 element"); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_funding_rate_all() { let (tx, rx) = std::sync::mpsc::channel(); tokio::task::spawn(async move { let ws_client = HuobiInverseSwapWSClient::new(tx, Some("wss://api.hbdm.com/swap-notification")) .await; ws_client.send(&[r#"{"topic":"public.*.funding_rate","op":"sub"}"#.to_string()]).await; // run for 60 seconds at most let _ = tokio::time::timeout(std::time::Duration::from_secs(60), ws_client.run()).await; ws_client.close().await; }); rx.into_iter().next().expect("should has at least 1 element"); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_overview() { gen_test_code!( HuobiInverseSwapWSClient, send, &[r#"{"sub":"market.overview","id":"crypto-ws-client"}"#.to_string()] ); } } #[cfg(test)] mod huobi_option { use crypto_ws_client::{HuobiOptionWSClient, WSClient}; #[test] #[ignore] fn subscribe() { gen_test_code!( HuobiOptionWSClient, subscribe, &[("trade.detail".to_string(), "BTC-USDT-210625-P-27000".to_string())] ); } #[test] #[ignore] fn subscribe_trade() { gen_test_code!( HuobiOptionWSClient, subscribe_trade, &["BTC-USDT-210625-P-27000".to_string()] ); } #[test] #[ignore] fn subscribe_ticker() { gen_test_code!( HuobiOptionWSClient, subscribe_ticker, &["BTC-USDT-210625-P-27000".to_string()] ); } #[test] #[ignore] fn subscribe_bbo() { gen_test_code!( HuobiOptionWSClient, subscribe_bbo, &["BTC-USDT-210625-P-27000".to_string()] ); } #[test] #[ignore] fn subscribe_orderbook() { gen_test_code!( HuobiOptionWSClient, subscribe_orderbook, &["BTC-USDT-210625-P-27000".to_string()] ); } #[test] #[ignore] fn subscribe_orderbook_topk() { gen_test_code!( HuobiOptionWSClient, subscribe_orderbook_topk, &["BTC-USDT-210625-P-27000".to_string()] ); } #[test] #[ignore] fn subscribe_candlestick() { gen_test_subscribe_candlestick!( HuobiOptionWSClient, &[("BTC-USDT-210625-P-27000".to_string(), 60)] ); gen_test_subscribe_candlestick!( HuobiOptionWSClient, &[("BTC-USDT-210625-P-27000".to_string(), 2592000)] ); } #[test] #[ignore] fn subscribe_overview() { gen_test_code!( HuobiOptionWSClient, send, &[r#"{"sub":"market.overview","id":"crypto-ws-client"}"#.to_string()] ); } } ================================================ FILE: crypto-ws-client/tests/kraken.rs ================================================ #[macro_use] mod utils; #[cfg(test)] mod kraken_spot { use crypto_ws_client::{KrakenSpotWSClient, WSClient}; #[tokio::test(flavor = "multi_thread")] async fn subscribe() { gen_test_code!( KrakenSpotWSClient, subscribe, &[ ("trade".to_string(), "XBT/USD".to_string()), ("ticker".to_string(), "XBT/USD".to_string()), ("spread".to_string(), "XBT/USD".to_string()), ("book".to_string(), "XBT/USD".to_string()) ] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_raw_json() { gen_test_code!( KrakenSpotWSClient, send, &[r#"{"event":"subscribe","pair":["XBT/USD"],"subscription":{"name":"trade"}}"# .to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_trade() { gen_test_code!( KrakenSpotWSClient, subscribe_trade, &["XBT/USD".to_string(), "ETH/USD".to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_ticker() { gen_test_code!( KrakenSpotWSClient, subscribe_ticker, &["XBT/USD".to_string(), "ETH/USD".to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_bbo() { gen_test_code!( KrakenSpotWSClient, subscribe_bbo, &["XBT/USD".to_string(), "ETH/USD".to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook() { gen_test_code!( KrakenSpotWSClient, subscribe_orderbook, &["XBT/USD".to_string(), "ETH/USD".to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_candlestick() { gen_test_subscribe_candlestick!( KrakenSpotWSClient, &[("XBT/USD".to_string(), 60), ("ETH/USD".to_string(), 60)] ); gen_test_subscribe_candlestick!( KrakenSpotWSClient, &[("XBT/USD".to_string(), 1296000), ("ETH/USD".to_string(), 1296000)] ); } } #[cfg(test)] mod kraken_inverse_swap { use crypto_ws_client::{KrakenFuturesWSClient, WSClient}; #[tokio::test(flavor = "multi_thread")] async fn subscribe_raw_json() { gen_test_code!( KrakenFuturesWSClient, send, &[r#"{"event":"subscribe","feed":"trade","product_ids":["PI_XBTUSD"]}"#.to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_trade() { gen_test_code!(KrakenFuturesWSClient, subscribe_trade, &["PI_XBTUSD".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_ticker() { gen_test_code!(KrakenFuturesWSClient, subscribe_ticker, &["PI_XBTUSD".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook() { gen_test_code!(KrakenFuturesWSClient, subscribe_orderbook, &["PI_XBTUSD".to_string()]); } } #[cfg(test)] mod kraken_inverse_future { use crypto_ws_client::{KrakenFuturesWSClient, WSClient}; #[tokio::test(flavor = "multi_thread")] async fn subscribe_raw_json() { gen_test_code!( KrakenFuturesWSClient, send, &[r#"{"event":"subscribe","feed":"trade","product_ids":["FI_XBTUSD_221230"]}"# .to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_trade() { gen_test_code!(KrakenFuturesWSClient, subscribe_trade, &["FI_XBTUSD_221230".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_ticker() { gen_test_code!(KrakenFuturesWSClient, subscribe_ticker, &["FI_XBTUSD_221230".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook() { gen_test_code!( KrakenFuturesWSClient, subscribe_orderbook, &["FI_XBTUSD_221230".to_string()] ); } } ================================================ FILE: crypto-ws-client/tests/kucoin.rs ================================================ #[macro_use] mod utils; #[cfg(test)] mod kucoin_spot { use crypto_ws_client::{KuCoinSpotWSClient, WSClient}; #[tokio::test(flavor = "multi_thread")] async fn subscribe() { gen_test_code!( KuCoinSpotWSClient, subscribe, &[("/market/match".to_string(), "BTC-USDT".to_string())] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_all_bbo() { gen_test_code!( KuCoinSpotWSClient, subscribe, &[("/market/ticker".to_string(), "all".to_string())] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_raw_json() { gen_test_code!( KuCoinSpotWSClient, send, &[r#"{"id":"crypto-ws-client","type":"subscribe","topic":"/market/match:BTC-USDT","privateChannel":false,"response":true}"#.to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_trade() { gen_test_code!( KuCoinSpotWSClient, subscribe_trade, &["BTC-USDT".to_string(), "ETH-USDT".to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_bbo() { gen_test_code!(KuCoinSpotWSClient, subscribe_bbo, &["BTC-USDT".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook() { gen_test_code!(KuCoinSpotWSClient, subscribe_orderbook, &["BTC-USDT".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook_topk() { gen_test_code!(KuCoinSpotWSClient, subscribe_orderbook_topk, &["BTC-USDT".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_ticker() { gen_test_code!(KuCoinSpotWSClient, subscribe_ticker, &["BTC-USDT".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_candlestick() { gen_test_subscribe_candlestick!( KuCoinSpotWSClient, &[("BTC-USDT".to_string(), 60), ("BTC-USDT".to_string(), 604800)] ); } } #[cfg(test)] mod kucoin_inverse_swap { use crypto_ws_client::{KuCoinSwapWSClient, WSClient}; #[tokio::test(flavor = "multi_thread")] async fn subscribe() { gen_test_code!( KuCoinSwapWSClient, subscribe, &[ ("/contractMarket/execution".to_string(), "XBTUSDM".to_string()), ("/contractMarket/execution".to_string(), "ETHUSDM".to_string()), ("/contractMarket/execution".to_string(), "DOTUSDM".to_string()), ("/contractMarket/execution".to_string(), "XRPUSDM".to_string()) ] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_raw_json() { gen_test_code!( KuCoinSwapWSClient, send, &[r#"{"id":"crypto-ws-client","type":"subscribe","topic":"/contractMarket/execution:XBTUSDM,ETHUSDM,DOTUSDM,XRPUSDM","privateChannel":false,"response":true}"#.to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_trade() { gen_test_code!( KuCoinSwapWSClient, subscribe_trade, &[ "XBTUSDM".to_string(), "ETHUSDM".to_string(), "DOTUSDM".to_string(), "XRPUSDM".to_string() ] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_bbo() { gen_test_code!(KuCoinSwapWSClient, subscribe_bbo, &["XBTUSDM".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook() { gen_test_code!(KuCoinSwapWSClient, subscribe_orderbook, &["XBTUSDM".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook_topk() { gen_test_code!(KuCoinSwapWSClient, subscribe_orderbook_topk, &["XBTUSDM".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_ticker() { gen_test_code!(KuCoinSwapWSClient, subscribe_ticker, &["XBTUSDM".to_string()]); } #[ignore] #[tokio::test(flavor = "multi_thread")] async fn subscribe_candlestick() { gen_test_subscribe_candlestick!( KuCoinSwapWSClient, &[ ("XBTUSDM".to_string(), 60), ("ETHUSDM".to_string(), 60), ("XBTUSDM".to_string(), 604800), ("ETHUSDM".to_string(), 604800) ] ); } } #[cfg(test)] mod kucoin_linear_swap { use crypto_ws_client::{KuCoinSwapWSClient, WSClient}; #[tokio::test(flavor = "multi_thread")] async fn subscribe() { gen_test_code!( KuCoinSwapWSClient, subscribe, &[("/contractMarket/execution".to_string(), "XBTUSDTM".to_string())] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_raw_json() { gen_test_code!( KuCoinSwapWSClient, send, &[r#"{"id":"crypto-ws-client","type":"subscribe","topic":"/contractMarket/execution:XBTUSDTM","privateChannel":false,"response":true}"#.to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_trade() { gen_test_code!(KuCoinSwapWSClient, subscribe_trade, &["XBTUSDTM".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_bbo() { gen_test_code!(KuCoinSwapWSClient, subscribe_bbo, &["XBTUSDTM".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook() { gen_test_code!(KuCoinSwapWSClient, subscribe_orderbook, &["XBTUSDTM".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook_topk() { gen_test_code!(KuCoinSwapWSClient, subscribe_orderbook_topk, &["XBTUSDTM".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_ticker() { gen_test_code!(KuCoinSwapWSClient, subscribe_ticker, &["XBTUSDTM".to_string()]); } #[ignore] #[tokio::test(flavor = "multi_thread")] async fn subscribe_candlestick() { gen_test_subscribe_candlestick!( KuCoinSwapWSClient, &[ ("XBTUSDTM".to_string(), 60), ("ETHUSDTM".to_string(), 60), ("XBTUSDTM".to_string(), 604800), ("ETHUSDTM".to_string(), 604800) ] ); } } #[cfg(test)] mod kucoin_inverse_future { use crypto_ws_client::{KuCoinSwapWSClient, WSClient}; #[ignore] #[tokio::test(flavor = "multi_thread")] async fn subscribe() { gen_test_code!( KuCoinSwapWSClient, subscribe, &[("/contractMarket/execution".to_string(), "XBTMZ22".to_string())] ); } #[ignore] #[tokio::test(flavor = "multi_thread")] async fn subscribe_raw_json() { gen_test_code!( KuCoinSwapWSClient, send, &[r#"{"id":"crypto-ws-client","type":"subscribe","topic":"/contractMarket/execution:XBTMZ22","privateChannel":false,"response":true}"#.to_string()] ); } #[ignore] #[tokio::test(flavor = "multi_thread")] async fn subscribe_trade() { gen_test_code!(KuCoinSwapWSClient, subscribe_trade, &["XBTMZ22".to_string()]); } #[ignore] #[tokio::test(flavor = "multi_thread")] async fn subscribe_bbo() { gen_test_code!(KuCoinSwapWSClient, subscribe_bbo, &["XBTMZ22".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook() { gen_test_code!(KuCoinSwapWSClient, subscribe_orderbook, &["XBTMZ22".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook_topk() { gen_test_code!(KuCoinSwapWSClient, subscribe_orderbook_topk, &["XBTMZ22".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_ticker() { gen_test_code!(KuCoinSwapWSClient, subscribe_ticker, &["XBTMZ22".to_string()]); } #[ignore] #[tokio::test(flavor = "multi_thread")] async fn subscribe_candlestick() { gen_test_subscribe_candlestick!( KuCoinSwapWSClient, &[("XBTMZ22".to_string(), 60), ("XBTMZ22".to_string(), 604800)] ); } } ================================================ FILE: crypto-ws-client/tests/mexc.rs ================================================ #[macro_use] mod utils; #[cfg(test)] mod mexc_spot { use crypto_ws_client::{MexcSpotWSClient, WSClient}; #[tokio::test(flavor = "multi_thread")] async fn subscribe() { gen_test_code!( MexcSpotWSClient, subscribe, &[ ("deal".to_string(), "BTC_USDT".to_string()), ("deal".to_string(), "ETH_USDT".to_string()), ("deal".to_string(), "MX_USDT".to_string()) ] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_raw_json() { gen_test_code!( MexcSpotWSClient, send, &[ r#"{"op":"sub.deal","symbol":"BTC_USDT"}"#.to_string(), r#"{"op":"sub.deal","symbol":"ETH_USDT"}"#.to_string(), r#"{"op":"sub.deal","symbol":"MX_USDT"}"#.to_string() ] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_trade() { gen_test_code!( MexcSpotWSClient, subscribe_trade, &["BTC_USDT".to_string(), "ETH_USDT".to_string(), "MX_USDT".to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook() { gen_test_code!( MexcSpotWSClient, subscribe_orderbook, &["BTC_USDT".to_string(), "ETH_USDT".to_string(), "MX_USDT".to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook_topk() { gen_test_code!(MexcSpotWSClient, subscribe_orderbook_topk, &["BTC_USDT".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_candlestick() { gen_test_subscribe_candlestick!( MexcSpotWSClient, &[ ("BTC_USDT".to_string(), 60), ("ETH_USDT".to_string(), 60), ("MX_USDT".to_string(), 60) ] ); gen_test_subscribe_candlestick!( MexcSpotWSClient, &[ ("BTC_USDT".to_string(), 2592000), ("ETH_USDT".to_string(), 2592000), ("MX_USDT".to_string(), 2592000) ] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_overview() { gen_test_code!(MexcSpotWSClient, send, &[r#"{"op":"sub.overview"}"#.to_string()]); } } #[cfg(test)] mod mexc_linear_swap { use crypto_ws_client::{MexcSwapWSClient, WSClient}; #[tokio::test(flavor = "multi_thread")] async fn subscribe() { gen_test_code!( MexcSwapWSClient, subscribe, &[ ("deal".to_string(), "BTC_USDT".to_string()), ("deal".to_string(), "ETH_USDT".to_string()) ] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_raw_json() { gen_test_code!( MexcSwapWSClient, send, &[r#"{"method":"sub.deal","param":{"symbol":"BTC_USDT"}}"#.to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_trade() { gen_test_code!(MexcSwapWSClient, subscribe_trade, &["BTC_USDT".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_ticker() { gen_test_code!(MexcSwapWSClient, subscribe_ticker, &["BTC_USDT".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook() { gen_test_code!(MexcSwapWSClient, subscribe_orderbook, &["BTC_USDT".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook_topk() { gen_test_code!(MexcSwapWSClient, subscribe_orderbook_topk, &["BTC_USDT".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_candlestick() { gen_test_subscribe_candlestick!(MexcSwapWSClient, &[("BTC_USDT".to_string(), 60)]); gen_test_subscribe_candlestick!(MexcSwapWSClient, &[("BTC_USDT".to_string(), 2592000)]); } } #[cfg(test)] mod mexc_inverse_swap { use crypto_ws_client::{MexcSwapWSClient, WSClient}; #[tokio::test(flavor = "multi_thread")] async fn subscribe_trade() { gen_test_code!(MexcSwapWSClient, subscribe_trade, &["BTC_USD".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_ticker() { gen_test_code!(MexcSwapWSClient, subscribe_ticker, &["BTC_USD".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook() { gen_test_code!(MexcSwapWSClient, subscribe_orderbook, &["BTC_USD".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook_topk() { gen_test_code!(MexcSwapWSClient, subscribe_orderbook_topk, &["BTC_USD".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_candlestick() { gen_test_subscribe_candlestick!(MexcSwapWSClient, &[("BTC_USD".to_string(), 60)]); gen_test_subscribe_candlestick!(MexcSwapWSClient, &[("BTC_USD".to_string(), 2592000)]); } } ================================================ FILE: crypto-ws-client/tests/okx.rs ================================================ use crypto_ws_client::{OkxWSClient, WSClient}; #[macro_use] mod utils; #[tokio::test(flavor = "multi_thread")] async fn okex_index() { gen_test_code!( OkxWSClient, subscribe, &[("index-tickers".to_string(), "BTC-USDT".to_string())] ); } #[cfg(test)] mod okx_spot { use crypto_ws_client::{OkxWSClient, WSClient}; #[tokio::test(flavor = "multi_thread")] async fn subscribe() { gen_test_code!(OkxWSClient, subscribe, &[("trades".to_string(), "BTC-USDT".to_string())]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_raw_json() { gen_test_code!( OkxWSClient, send, &[r#"{"op":"subscribe","args":[{"channel":"trades","instId":"BTC-USDT"}]}"# .to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_trade() { gen_test_code!(OkxWSClient, subscribe_trade, &["BTC-USDT".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_ticker() { gen_test_code!(OkxWSClient, subscribe_ticker, &["BTC-USDT".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_bbo() { gen_test_code!(OkxWSClient, subscribe_bbo, &["BTC-USDT".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook() { gen_test_code!(OkxWSClient, subscribe_orderbook, &["BTC-USDT".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook_topk() { gen_test_code!(OkxWSClient, subscribe_orderbook_topk, &["BTC-USDT".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_candlestick() { gen_test_subscribe_candlestick!(OkxWSClient, &[("BTC-USDT".to_string(), 60)]); gen_test_subscribe_candlestick!(OkxWSClient, &[("BTC-USDT".to_string(), 604800)]); } } #[cfg(test)] mod okx_future { use crypto_ws_client::{OkxWSClient, WSClient}; #[tokio::test(flavor = "multi_thread")] async fn subscribe() { gen_test_code!( OkxWSClient, subscribe, &[("trades".to_string(), "BTC-USDT-221230".to_string())] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_trade() { gen_test_code!(OkxWSClient, subscribe_trade, &["BTC-USDT-221230".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_ticker() { gen_test_code!(OkxWSClient, subscribe_ticker, &["BTC-USDT-221230".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_bbo() { gen_test_code!(OkxWSClient, subscribe_bbo, &["BTC-USDT-221230".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook() { gen_test_code!(OkxWSClient, subscribe_orderbook, &["BTC-USDT-221230".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook_topk() { gen_test_code!(OkxWSClient, subscribe_orderbook_topk, &["BTC-USDT-221230".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_candlestick() { gen_test_subscribe_candlestick!(OkxWSClient, &[("BTC-USDT-221230".to_string(), 60)]); gen_test_subscribe_candlestick!(OkxWSClient, &[("BTC-USDT-221230".to_string(), 604800)]); } } #[cfg(test)] mod okx_swap { use crypto_ws_client::{OkxWSClient, WSClient}; #[tokio::test(flavor = "multi_thread")] async fn subscribe() { gen_test_code!( OkxWSClient, subscribe, &[("trades".to_string(), "BTC-USDT-SWAP".to_string())] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_trade() { gen_test_code!(OkxWSClient, subscribe_trade, &["BTC-USDT-SWAP".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_ticker() { gen_test_code!(OkxWSClient, subscribe_ticker, &["BTC-USDT-SWAP".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_bbo() { gen_test_code!(OkxWSClient, subscribe_bbo, &["BTC-USDT-SWAP".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook() { gen_test_code!(OkxWSClient, subscribe_orderbook, &["BTC-USDT-SWAP".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook_topk() { gen_test_code!(OkxWSClient, subscribe_orderbook_topk, &["BTC-USDT-SWAP".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_candlestick() { gen_test_subscribe_candlestick!(OkxWSClient, &[("BTC-USDT-SWAP".to_string(), 60)]); gen_test_subscribe_candlestick!(OkxWSClient, &[("BTC-USDT-SWAP".to_string(), 604800)]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_funding_rate() { gen_test_code!( OkxWSClient, subscribe, &[("funding-rate".to_string(), "BTC-USDT-SWAP".to_string())] ); } } #[cfg(test)] mod okx_option { use crypto_ws_client::{OkxWSClient, WSClient}; #[tokio::test(flavor = "multi_thread")] #[ignore = "lack of liquidity"] async fn subscribe_trade() { gen_test_code!(OkxWSClient, subscribe_trade, &["BTC-USD-221230-50000-C".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_ticker() { gen_test_code!(OkxWSClient, subscribe_ticker, &["BTC-USD-221230-50000-C".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_bbo() { gen_test_code!(OkxWSClient, subscribe_bbo, &["BTC-USD-221230-50000-C".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook() { gen_test_code!(OkxWSClient, subscribe_orderbook, &["BTC-USD-221230-50000-C".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook_topk() { gen_test_code!( OkxWSClient, subscribe_orderbook_topk, &["BTC-USD-221230-50000-C".to_string()] ); } #[tokio::test(flavor = "multi_thread")] #[ignore = "lack of liquidity"] async fn subscribe_candlestick() { gen_test_subscribe_candlestick!(OkxWSClient, &[("BTC-USD-221230-50000-C".to_string(), 60)]); gen_test_subscribe_candlestick!( OkxWSClient, &[("BTC-USD-221230-50000-C".to_string(), 604800)] ); } } ================================================ FILE: crypto-ws-client/tests/utils/mod.rs ================================================ macro_rules! gen_test_code { ($client:ident, $func_name:ident, $symbols:expr) => { let (tx, rx) = std::sync::mpsc::channel(); tokio::task::spawn(async move { let ws_client = $client::new(tx, None).await; ws_client.$func_name($symbols).await; // run for 60 seconds at most let _ = tokio::time::timeout(std::time::Duration::from_secs(60), ws_client.run()).await; ws_client.close().await; }); let mut messages = Vec::::new(); for msg in rx { messages.push(msg); break; } assert!(!messages.is_empty()); }; } #[allow(unused_macros)] macro_rules! gen_test_subscribe_candlestick { ($client:ident, $symbol_interval_list:expr) => { let (tx, rx) = std::sync::mpsc::channel(); tokio::task::spawn(async move { let ws_client = $client::new(tx, None).await; ws_client.subscribe_candlestick($symbol_interval_list).await; // run for 60 seconds at most let _ = tokio::time::timeout(std::time::Duration::from_secs(60), ws_client.run()).await; ws_client.close().await; }); let mut messages = Vec::::new(); for msg in rx { messages.push(msg); break; } assert!(!messages.is_empty()); }; } ================================================ FILE: crypto-ws-client/tests/zb.rs ================================================ #[macro_use] mod utils; #[cfg(test)] mod zb_spot { use crypto_ws_client::{WSClient, ZbSpotWSClient}; #[tokio::test(flavor = "multi_thread")] async fn subscribe() { gen_test_code!( ZbSpotWSClient, subscribe, &[("trades".to_string(), "btc_usdt".to_string())] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_raw_json() { gen_test_code!( ZbSpotWSClient, send, &[r#"{"event":"addChannel","channel":"btcusdt_trades"}"#.to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_trade() { gen_test_code!(ZbSpotWSClient, subscribe_trade, &["btc_usdt".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook_topk() { gen_test_code!(ZbSpotWSClient, subscribe_orderbook_topk, &["btc_usdt".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_ticker() { gen_test_code!(ZbSpotWSClient, subscribe_ticker, &["btc_usdt".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_candlestick() { gen_test_subscribe_candlestick!(ZbSpotWSClient, &[("btc_usdt".to_string(), 60)]); gen_test_subscribe_candlestick!(ZbSpotWSClient, &[("btc_usdt".to_string(), 604800)]); } } #[cfg(test)] mod zb_linear_swap { use crypto_ws_client::{WSClient, ZbSwapWSClient}; #[tokio::test(flavor = "multi_thread")] async fn subscribe() { gen_test_code!( ZbSwapWSClient, subscribe, &[ ("Trade".to_string(), "BTC_USDT".to_string()), ("Depth".to_string(), "BTC_USDT".to_string()) ] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_raw_json() { gen_test_code!( ZbSwapWSClient, send, &[ r#"{"action":"subscribe", "channel":"BTC_USDT.Trade", "size":100}"#.to_string(), r#"{"action":"subscribe", "channel":"BTC_USDT.Depth", "size":200}"#.to_string() ] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_trade() { gen_test_code!( ZbSwapWSClient, subscribe_trade, &["BTC_USDT".to_string(), "ETH_USDT".to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook() { gen_test_code!( ZbSwapWSClient, subscribe_orderbook, &["BTC_USDT".to_string(), "ETH_USDT".to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook_topk() { gen_test_code!( ZbSwapWSClient, subscribe_orderbook_topk, &["BTC_USDT".to_string(), "ETH_USDT".to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_ticker() { gen_test_code!( ZbSwapWSClient, subscribe_ticker, &["BTC_USDT".to_string(), "ETH_USDT".to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_candlestick() { gen_test_subscribe_candlestick!( ZbSwapWSClient, &[("BTC_USDT".to_string(), 60), ("ETH_USDT".to_string(), 60)] ); } } ================================================ FILE: crypto-ws-client/tests/zbg.rs ================================================ #[macro_use] mod utils; #[cfg(test)] mod zbg_spot { use crypto_ws_client::{WSClient, ZbgSpotWSClient}; #[tokio::test(flavor = "multi_thread")] async fn subscribe() { gen_test_code!( ZbgSpotWSClient, subscribe, &[("TRADE".to_string(), "btc_usdt".to_string())] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_raw_json() { gen_test_code!( ZbgSpotWSClient, send, &[r#"{"action":"ADD", "dataType":329_TRADE_BTC_USDT}"#.to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_trade() { gen_test_code!(ZbgSpotWSClient, subscribe_trade, &["btc_usdt".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_orderbook() { gen_test_code!(ZbgSpotWSClient, subscribe_orderbook, &["btc_usdt".to_string()]); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_ticker() { gen_test_code!(ZbgSpotWSClient, subscribe_ticker, &["btc_usdt".to_string()]); } #[ignore] #[tokio::test(flavor = "multi_thread")] async fn subscribe_ticker_all() { gen_test_code!( ZbgSpotWSClient, send, &[r#"{"action":"ADD", "dataType":"ALL_TRADE_STATISTIC_24H"}"#.to_string()] ); } #[tokio::test(flavor = "multi_thread")] async fn subscribe_candlestick() { gen_test_subscribe_candlestick!(ZbgSpotWSClient, &[("btc_usdt".to_string(), 60)]); gen_test_subscribe_candlestick!(ZbgSpotWSClient, &[("btc_usdt".to_string(), 604800)]); } } #[cfg(test)] mod zbg_inverse_swap { use crypto_ws_client::{WSClient, ZbgSwapWSClient}; #[test] #[ignore] fn subscribe() { gen_test_code!( ZbgSwapWSClient, subscribe, &[ ("future_tick".to_string(), "1000001".to_string()), ("future_tick".to_string(), "1000003".to_string()) ] ); } #[test] #[ignore] fn subscribe_raw_json() { gen_test_code!( ZbgSwapWSClient, send, &[ r#"{"action":"sub", "topic":"future_tick-1000001"}"#.to_string(), r#"{"action":"sub", "topic":"future_tick-1000003"}"#.to_string() ] ); } #[test] #[ignore] fn subscribe_trade() { gen_test_code!( ZbgSwapWSClient, subscribe_trade, &["BTC_USD-R".to_string(), "ETH_USD-R".to_string()] ); } #[test] #[ignore] fn subscribe_orderbook() { gen_test_code!( ZbgSwapWSClient, subscribe_orderbook, &["BTC_USD-R".to_string(), "ETH_USD-R".to_string()] ); } #[ignore] #[tokio::test(flavor = "multi_thread")] async fn subscribe_ticker() { gen_test_code!( ZbgSwapWSClient, subscribe_ticker, &["BTC_USD-R".to_string(), "ETH_USD-R".to_string()] ); } #[ignore] #[tokio::test(flavor = "multi_thread")] async fn subscribe_ticker_all() { gen_test_code!( ZbgSwapWSClient, send, &[r#"{"action":"sub", "topic":"future_all_indicator"}"#.to_string()] ); } #[ignore] #[tokio::test(flavor = "multi_thread")] async fn subscribe_candlestick() { gen_test_subscribe_candlestick!( ZbgSwapWSClient, &[("BTC_USD-R".to_string(), 60), ("ETH_USD-R".to_string(), 60)] ); gen_test_subscribe_candlestick!( ZbgSwapWSClient, &[("BTC_USD-R".to_string(), 604800), ("ETH_USD-R".to_string(), 604800)] ); } } #[cfg(test)] mod zbg_linear_swap { use crypto_ws_client::{WSClient, ZbgSwapWSClient}; #[test] #[ignore] fn subscribe() { gen_test_code!( ZbgSwapWSClient, subscribe, &[ ("future_tick".to_string(), "1000000".to_string()), ("future_tick".to_string(), "1000002".to_string()) ] ); } #[test] #[ignore] fn subscribe_raw_json() { gen_test_code!( ZbgSwapWSClient, send, &[ r#"{"action":"sub", "topic":"future_tick-1000000"}"#.to_string(), r#"{"action":"sub", "topic":"future_tick-1000002"}"#.to_string() ] ); } #[test] #[ignore] fn subscribe_trade() { gen_test_code!( ZbgSwapWSClient, subscribe_trade, &["BTC_USDT".to_string(), "ETH_USDT".to_string()] ); } #[test] #[ignore] fn subscribe_orderbook() { gen_test_code!( ZbgSwapWSClient, subscribe_orderbook, &["BTC_USDT".to_string(), "ETH_USDT".to_string()] ); } #[ignore] #[tokio::test(flavor = "multi_thread")] async fn subscribe_ticker() { gen_test_code!( ZbgSwapWSClient, subscribe_ticker, &["BTC_USDT".to_string(), "ETH_USDT".to_string()] ); } #[ignore] #[tokio::test(flavor = "multi_thread")] async fn subscribe_ticker_all() { gen_test_code!( ZbgSwapWSClient, send, &[r#"{"action":"sub", "topic":"future_all_indicator"}"#.to_string()] ); } #[ignore] #[tokio::test(flavor = "multi_thread")] async fn subscribe_candlestick() { gen_test_subscribe_candlestick!( ZbgSwapWSClient, &[("BTC_USDT".to_string(), 60), ("ETH_USDT".to_string(), 60)] ); gen_test_subscribe_candlestick!( ZbgSwapWSClient, &[("BTC_USDT".to_string(), 604800), ("ETH_USDT".to_string(), 604800)] ); } } ================================================ FILE: rustfmt.toml ================================================ edition = "2021" version = "Two" use_small_heuristics = "Max" newline_style = "Unix" wrap_comments = true format_generated_files = false imports_granularity="Crate"