[
  {
    "path": ".config/nextest.toml",
    "content": "[profile.default]\nslow-timeout = { period = \"60s\", terminate-after = 2 }\nfail-fast = false\nretries = 1\n\n"
  },
  {
    "path": ".editorconfig",
    "content": "# https://EditorConfig.org\nroot = true\n\n[*]\ncharset = utf-8\nend_of_line = lf\nindent_size = 4\nindent_style = space\ninsert_final_newline = true\ntrim_trailing_whitespace = true\n\n# Markdown Files\n[*.md]\ntrim_trailing_whitespace = false\n\n# Batch Files\n[*.{cmd,bat}]\nend_of_line = crlf\n"
  },
  {
    "path": ".github/CODEOWNERS",
    "content": "*\t@soulmachine\n\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non: [push, pull_request]\n\nenv:\n  CARGO_TERM_COLOR: always\n\n# Stop the previous CI tasks (which is deprecated)\n# to conserve the runner resource.\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\njobs:\n  build:\n    name: Cargo build\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v2\n      - uses: actions-rs/toolchain@v1\n        with:\n          toolchain: nightly\n          override: true\n      - uses: actions-rs/cargo@v1\n        with:\n          command: build\n          args: --release --all-features\n\n  test:\n    name: Cargo test\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v2\n      - uses: actions-rs/toolchain@v1\n        with:\n          toolchain: nightly\n          override: true\n      - uses: taiki-e/install-action@nextest\n      - uses: actions-rs/cargo@v1\n        with:\n          command: build\n      - name: Run cargo nextest\n        run: |\n          # Run all tests except:\n          # * bitmex, because bitmex has very low rate limit\n          # * FTX, the FTX website is not operational\n          # * zbg, due to the \"invalid peer certificate: UnknownIssuer\" error\n          cargo nextest run -E 'all() - binary(~bitmex) - binary(~ftx) - binary(~zbg)'\n\n          # Run the '*bitmex*' tests in -j1.\n          cargo nextest run -E 'binary(~bitmex)' -j1 || true\n\n  doc-test:\n    name: Cargo doctest\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v2\n      - uses: actions-rs/toolchain@v1\n        with:\n          toolchain: nightly\n          override: true\n      - uses: actions-rs/cargo@v1\n        with:\n          command: test\n          args: --doc\n\n  fmt:\n    name: Cargo fmt\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v2\n      - uses: actions-rs/toolchain@v1\n        with:\n          toolchain: nightly\n          override: true\n          components: rustfmt\n      - uses: actions-rs/cargo@v1\n        with:\n          command: fmt\n          args: -- --check\n\n  check:\n    name: Cargo check\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v2\n      - uses: actions-rs/toolchain@v1\n        with:\n          toolchain: nightly\n          override: true\n      - uses: actions-rs/cargo@v1\n        with:\n          command: check\n\n  clippy:\n    name: Cargo clippy\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v2\n      - uses: actions-rs/toolchain@v1\n        with:\n          toolchain: nightly\n          override: true\n          components: clippy\n      - uses: actions-rs/cargo@v1\n        with:\n          command: clippy\n"
  },
  {
    "path": ".gitignore",
    "content": "# Generated by Cargo\n# will have compiled files and executables\n/target/\n\n# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries\n# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html\nCargo.lock\n\n# These are backup files generated by rustfmt\n**/*.rs.bk\n\n# IDEA configurations\n/.idea\n*.iml\n*.project\n*.classpath\n"
  },
  {
    "path": "Cargo.toml",
    "content": "[workspace]\nmembers = [\n  \"crypto-client\",\n  \"crypto-crawler\",\n  \"crypto-markets\",\n  \"crypto-market-type\",\n  \"crypto-msg-type\",\n  \"crypto-rest-client\",\n  \"crypto-ws-client\",\n]\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "# crypto-crawler-rs\n\n[![](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)\n[![](https://img.shields.io/crates/v/crypto-crawler.svg)](https://crates.io/crates/crypto-crawler)\n[![](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)\n==========\n\nA rock-solid cryprocurrency crawler.\n\n## Quickstart\n\nUse the [carbonbot](https://github.com/crypto-crawler/carbonbot) binary to crawl data.\n\nIf you need more control and customization, use this library.\n\n## Architecture\n\n![](./dependency-tree.svg)\n\n- [crypto-crawler](./crypto-crawler) is the crawler library to crawl websocket and restful messages from exchanges\n- [carbonbot](https://github.com/crypto-crawler/carbonbot) is the main CLI tool to run crawlers.\n- [crypto-ws-client](./crypto-ws-client) is the underlying websocket client library, providing a set of universal APIs for different exchanges.\n- [crypto-rest-client](./crypto-rest-client) is the underlying RESTful client library, providing universal APIs to get public data from different exchanges.\n- [crypto-markets](./crypto-markets) is a RESTful library to retreive market meta data from cryptocurrency echanges.\n- [crypto-client](./crypto-client) is a RESTful client library to place and cancel orders.\n- 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.\n\n## How to parse raw messages\n\nUse the [crypto-msg-parser](https://github.com/crypto-crawler/crypto-msg-parser) library to parse raw messages.\n\nCrawlers should always preserve the original data without any parsing.\n"
  },
  {
    "path": "crypto-client/Cargo.toml",
    "content": "[package]\nname = \"crypto-client\"\nversion = \"0.1.0\"\nauthors = [\"soulmachine <soulmachine@gmail.com>\"]\nedition = \"2021\"\ndescription   = \"An unified restful client for all cryptocurrency exchanges.\"\nlicense = \"Apache-2.0\"\nrepository = \"https://github.com/crypto-crawler/crypto-crawler-rs/tree/main/crypto-client\"\nhomepage = \"https://github.com/crypto-crawler/crypto-crawler-rs/tree/main/crypto-client\"\n\n[dependencies]\nreqwest = { version = \"0.11.11\", features = [\"blocking\", \"json\"] }\nserde = { version = \"1.0.140\", features = [\"derive\"] }\nserde_json = \"1.0.82\"\n"
  },
  {
    "path": "crypto-client/README.md",
    "content": "# crypto-client\n\nAn unified client for all cryptocurrency exchanges.\n\n## Example\n\n```rust\nuse crypto_client::{CryptoClient, MarketType};\n\nfn main() {\n    let config: HashMap<&str, &str> = vec![\n        (\"eosAccount\", \"your-eos-account\"),\n        (\"eosPrivateKey\", \"your-eos-private-key\"),\n    ].into_iter().collect();\n\n    let crypto_client = CryptoClient::new(config);\n\n    // buy\n    let transaction_id = crypto_client.place_order(\n        { exchange: \"Newdex\", pair: \"EIDOS_EOS\", market_type: \"Spot\" },\n        0.00121,\n        9.2644,\n        false,\n    );\n    println!(\"{}\", transactionId);\n}\n```\n\n## Supported Exchanges\n\n- Binance\n- Huobi\n- OKEx\n- WhaleEx\n"
  },
  {
    "path": "crypto-client/src/lib.rs",
    "content": "/// An unified restful client for all cryptocurrency exchanges.\npub struct CryptoClient {\n    #[allow(dead_code)]\n    exchange: String,\n}\n\n#[cfg(test)]\nmod tests {\n    #[test]\n    fn it_works() {\n        assert_eq!(2 + 2, 4);\n    }\n}\n"
  },
  {
    "path": "crypto-crawler/Cargo.toml",
    "content": "[package]\nname = \"crypto-crawler\"\nversion = \"4.7.9\"\nauthors = [\"soulmachine <soulmachine@gmail.com>\"]\nedition = \"2021\"\ndescription   = \"A rock-solid cryprocurrency crawler.\"\nlicense = \"Apache-2.0\"\nrepository = \"https://github.com/crypto-crawler/crypto-crawler-rs/tree/main/crypto-crawler\"\nkeywords = [\"cryptocurrency\", \"blockchain\", \"trading\"]\n\n[dependencies]\ncrypto-markets = \"1.3.11\"\ncrypto-market-type = \"1.1.5\"\ncrypto-msg-parser = \"2.8.26\"\ncrypto-msg-type = \"1.0.11\"\ncrypto-pair = \"2.3.13\"\ncrypto-rest-client = \"1.0.1\"\ncrypto-ws-client = \"4.12.11\"\nfslock = \"0.2.1\"\nonce_cell = \"1.17.1\"\nlog = \"0.4.17\"\nrand = \"0.8.5\"\nreqwest = { version = \"0.11.14\", features = [\"blocking\", \"gzip\"] }\nserde = { version = \"1.0.157\", features = [\"derive\"] }\nserde_json = \"1.0.94\"\ntokio = { version = \"1.26.0\", features = [\"macros\", \"rt-multi-thread\", \"sync\", \"time\"] }\n\n[dev_dependencies]\nenv_logger = \"0.9\"\ntest-case = \"1\"\ntokio = { version = \"1\", features = [\"test-util\"] }\n"
  },
  {
    "path": "crypto-crawler/README.md",
    "content": "# crypto-crawler\n\n[![](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)\n[![](https://img.shields.io/crates/v/crypto-crawler.svg)](https://crates.io/crates/crypto-crawler)\n[![](https://docs.rs/crypto-crawler/badge.svg)](https://docs.rs/crypto-crawler)\n==========\n\nA rock-solid cryprocurrency crawler.\n\n## Crawl realtime trades\n\n```rust\nuse crypto_crawler::{crawl_trade, MarketType};\n\n#[tokio::main(flavor = \"multi_thread\")]\nasync fn main() {\n    let (tx, rx) = std::sync::mpsc::channel();\n    tokio::task::spawn(async move {\n        for msg in rx {\n            println!(\"{}\", msg);\n        }\n    });\n\n    // Crawl realtime trades for all symbols of binance inverse_swap markets\n    crawl_trade(\"binance\", MarketType::InverseSwap, None, tx).await;\n}\n```\n\n## Crawl realtime level2 orderbook incremental updates\n\n```rust\nuse crypto_crawler::{crawl_l2_event, MarketType};\n\n#[tokio::main(flavor = \"multi_thread\")]\nasync fn main() {\n    let (tx, rx) = std::sync::mpsc::channel();\n    tokio::task::spawn(async move {\n        for msg in rx {\n            println!(\"{}\", msg);\n        }\n    });\n\n    // Crawl realtime level2 incremental updates for all symbols of binance inverse_swap markets\n    crawl_l2_event(\"binance\", MarketType::InverseSwap, None, tx).await;\n}\n```\n\n## Crawl level2 orderbook full snapshots from RESTful API\n\n```rust\nuse crypto_crawler::{crawl_l2_snapshot, MarketType};\n\nfn main() {\n    let (tx, rx) = std::sync::mpsc::channel();\n    std::thread::spawn(move || {\n        for msg in rx {\n            println!(\"{}\", msg);\n        }\n    });\n\n    // Crawl level2 full snapshots for all symbols of binance inverse_swap markets\n    crawl_l2_snapshot(\"binance\", MarketType::InverseSwap, None, tx);\n}\n```\n\n## Crawl realtime level2 orderbook top-K snapshots\n\n```rust\nuse crypto_crawler::{crawl_l2_topk, MarketType};\n\n#[tokio::main(flavor = \"multi_thread\")]\nasync fn main() {\n    let (tx, rx) = std::sync::mpsc::channel();\n    tokio::task::spawn(async move {\n        for msg in rx {\n            println!(\"{}\", msg);\n        }\n    });\n\n    // Crawl realtime level2 top-k snapshots for all symbols of binance inverse_swap markets\n    crawl_l2_topk(\"binance\", MarketType::InverseSwap, None, tx).await;\n}\n```\n\n## Crawl realtime level3 orderbook incremental updates\n\n```rust\nuse crypto_crawler::{crawl_l3_event, MarketType};\n\n#[tokio::main(flavor = \"multi_thread\")]\nasync fn main() {\n    let (tx, rx) = std::sync::mpsc::channel();\n    tokio::task::spawn(async move {\n        for msg in rx {\n            println!(\"{}\", msg);\n        }\n    });\n\n    // Crawl realtime level3 updates for all symbols of CoinbasePro spot market\n    crawl_l3_event(\"coinbase_pro\", MarketType::Spot, None, tx).await;\n}\n```\n\n## Crawl level3 orderbook full snapshots from RESTful API\n\n```rust\nuse crypto_crawler::{crawl_l3_snapshot, MarketType};\n\nfn main() {\n    let (tx, rx) = std::sync::mpsc::channel();\n    std::thread::spawn(move || {\n        for msg in rx {\n            println!(\"{}\", msg);\n        }\n    });\n\n    // Crawl level3 orderbook full snapshots for all symbols of CoinbasePro spot markets\n    crawl_l3_snapshot(\"coinbase_pro\", MarketType::Spot, None, tx);\n}\n```\n\n## Crawl realtime BBO\n\n```rust\nuse crypto_crawler::{crawl_bbo, MarketType};\n\n#[tokio::main(flavor = \"multi_thread\")]\nasync fn main() {\n    let (tx, rx) = std::sync::mpsc::channel();\n    tokio::task::spawn(async move {\n        for msg in rx {\n            println!(\"{}\", msg);\n        }\n    });\n\n    // Crawl realtime best bid and ask messages for all symbols of binance COIN-margined perpetual markets\n    crawl_bbo(\"binance\", MarketType::InverseSwap, None, tx).await;\n}\n```\n\n## Crawl 24hr rolling window tickers\n\n```rust\nuse crypto_crawler::{crawl_ticker, MarketType};\n\n#[tokio::main(flavor = \"multi_thread\")]\nasync fn main() {\n    let (tx, rx) = std::sync::mpsc::channel();\n    tokio::task::spawn(async move {\n        for msg in rx {\n            println!(\"{}\", msg);\n        }\n    });\n\n    // Crawl 24hr rolling window tickers for all symbols of binance COIN-margined perpetual markets\n    crawl_ticker(\"binance\", MarketType::InverseSwap, None, tx).await;\n}\n```\n\n## Crawl candlesticks(i.e., OHLCV)\n\n```rust\nuse crypto_crawler::{crawl_candlestick, MarketType};\n\n#[tokio::main(flavor = \"multi_thread\")]\nasync fn main() {\n    let (tx, rx) = std::sync::mpsc::channel();\n    tokio::task::spawn(async move {\n        for msg in rx {\n            println!(\"{}\", msg);\n        }\n    });\n\n    // Crawl candlesticks from 1 minute to 3 minutes for all symbols of binance COIN-margined perpetual markets\n    crawl_candlestick(\"binance\", MarketType::InverseSwap, None, tx).await;\n}\n```\n\n## Crawl funding rates\n\n```rust\nuse crypto_crawler::{crawl_funding_rate, MarketType};\n\n#[tokio::main(flavor = \"multi_thread\")]\nasync fn main() {\n    let (tx, rx) = std::sync::mpsc::channel();\n    tokio::task::spawn(async move {\n        for msg in rx {\n            println!(\"{}\", msg);\n        }\n    });\n\n    // Crawl funding rates for all symbols of binance COIN-margined perpetual markets\n    crawl_funding_rate(\"binance\", MarketType::InverseSwap, None, tx).await;\n}\n```\n"
  },
  {
    "path": "crypto-crawler/src/crawlers/binance.rs",
    "content": "use core::panic;\nuse std::sync::mpsc::Sender;\n\nuse crate::{\n    crawlers::utils::crawl_event, fetch_symbols_retry, get_hot_spot_symbols, msg::Message,\n    utils::cmc_rank::sort_by_cmc_rank,\n};\nuse crypto_market_type::MarketType;\nuse crypto_msg_type::MessageType;\nuse crypto_ws_client::*;\n\nuse super::utils::create_conversion_thread;\n\nconst EXCHANGE_NAME: &str = \"binance\";\n\npub(crate) async fn crawl_trade(\n    market_type: MarketType,\n    symbols: Option<&[String]>,\n    tx: Sender<Message>,\n) {\n    if market_type == MarketType::EuropeanOption\n        && (symbols.is_none() || symbols.unwrap().is_empty())\n    {\n        let tx = create_conversion_thread(\n            EXCHANGE_NAME.to_string(),\n            MessageType::Trade,\n            market_type,\n            tx,\n        );\n        let topics: Vec<(String, String)> = vec![\n            // (\"TICKER_ALL\".to_string(), \"BTCUSDT\".to_string()),\n            (\"TRADE_ALL\".to_string(), \"BTCUSDT_C\".to_string()),\n            (\"TRADE_ALL\".to_string(), \"BTCUSDT_P\".to_string()),\n        ];\n\n        let ws_client = BinanceOptionWSClient::new(tx, None).await;\n        ws_client.subscribe(&topics).await;\n        ws_client.run().await;\n        ws_client.close().await;\n    } else {\n        crawl_event(EXCHANGE_NAME, MessageType::Trade, market_type, symbols, tx).await;\n    }\n}\n\npub(crate) async fn crawl_bbo(\n    market_type: MarketType,\n    symbols: Option<&[String]>,\n    tx: Sender<Message>,\n) {\n    if symbols.is_none() || symbols.unwrap().is_empty() {\n        if market_type == MarketType::Spot {\n            // spot `!bookTicker` has been removed since December 7, 2022\n            let mut hot_spot_symbols = tokio::task::block_in_place(move || {\n                let spot_symbols = fetch_symbols_retry(EXCHANGE_NAME, market_type);\n                get_hot_spot_symbols(EXCHANGE_NAME, &spot_symbols)\n            });\n            sort_by_cmc_rank(EXCHANGE_NAME, &mut hot_spot_symbols);\n            let symbols = Some(hot_spot_symbols.as_slice());\n            crawl_event(EXCHANGE_NAME, MessageType::BBO, market_type, symbols, tx).await\n        } else {\n            let tx = create_conversion_thread(\n                EXCHANGE_NAME.to_string(),\n                MessageType::BBO,\n                market_type,\n                tx,\n            );\n            let commands =\n                vec![r#\"{\"id\":9527,\"method\":\"SUBSCRIBE\",\"params\":[\"!bookTicker\"]}\"#.to_string()]; // All Book Tickers Stream\n            match market_type {\n                MarketType::InverseFuture | MarketType::InverseSwap => {\n                    let ws_client = BinanceInverseWSClient::new(tx, None).await;\n                    ws_client.send(&commands).await;\n                    ws_client.run().await;\n                    ws_client.close().await;\n                }\n                MarketType::LinearFuture | MarketType::LinearSwap => {\n                    let ws_client = BinanceLinearWSClient::new(tx, None).await;\n                    ws_client.send(&commands).await;\n                    ws_client.run().await;\n                    ws_client.close().await;\n                }\n                _ => panic!(\"Binance {} market does NOT have the BBO channel\", market_type),\n            }\n        }\n    } else {\n        crawl_event(EXCHANGE_NAME, MessageType::BBO, market_type, symbols, tx).await;\n    }\n}\n\npub(crate) async fn crawl_ticker(\n    market_type: MarketType,\n    symbols: Option<&[String]>,\n    tx: Sender<Message>,\n) {\n    if symbols.is_none() || symbols.unwrap().is_empty() {\n        let tx = create_conversion_thread(\n            EXCHANGE_NAME.to_string(),\n            MessageType::Ticker,\n            market_type,\n            tx,\n        );\n        let commands =\n            vec![r#\"{\"id\":9527,\"method\":\"SUBSCRIBE\",\"params\":[\"!ticker@arr\"]}\"#.to_string()];\n\n        match market_type {\n            MarketType::Spot => {\n                let ws_client = BinanceSpotWSClient::new(tx, None).await;\n                ws_client.send(&commands).await;\n                ws_client.run().await;\n                ws_client.close().await;\n            }\n            MarketType::InverseFuture | MarketType::InverseSwap => {\n                let ws_client = BinanceInverseWSClient::new(tx, None).await;\n                ws_client.send(&commands).await;\n                ws_client.run().await;\n                ws_client.close().await;\n            }\n            MarketType::LinearFuture | MarketType::LinearSwap => {\n                let ws_client = BinanceLinearWSClient::new(tx, None).await;\n                ws_client.send(&commands).await;\n                ws_client.run().await;\n                ws_client.close().await;\n            }\n            MarketType::EuropeanOption => {\n                let commands = vec![\n                    r#\"{\"id\":9527,\"method\":\"SUBSCRIBE\",\"params\":[\"BTCUSDT@TICKER_ALL\"]}\"#\n                        .to_string(),\n                ];\n                let ws_client = BinanceLinearWSClient::new(tx, None).await;\n                ws_client.send(&commands).await;\n                ws_client.run().await;\n                ws_client.close().await;\n            }\n            _ => panic!(\"Binance {} market does NOT have the ticker channel\", market_type),\n        }\n    } else {\n        crawl_event(EXCHANGE_NAME, MessageType::Ticker, market_type, symbols, tx).await;\n    }\n}\n\n#[allow(clippy::unnecessary_unwrap)]\npub(crate) async fn crawl_funding_rate(\n    market_type: MarketType,\n    symbols: Option<&[String]>,\n    tx: Sender<Message>,\n) {\n    let tx = create_conversion_thread(\n        EXCHANGE_NAME.to_string(),\n        MessageType::FundingRate,\n        market_type,\n        tx,\n    );\n    let ws_client: Box<dyn WSClient + Send + Sync> = match market_type {\n        MarketType::InverseSwap => Box::new(BinanceInverseWSClient::new(tx, None).await),\n        MarketType::LinearSwap => Box::new(BinanceLinearWSClient::new(tx, None).await),\n        _ => panic!(\"Binance {} does NOT have funding rates\", market_type),\n    };\n\n    if symbols.is_none() || symbols.unwrap().is_empty() {\n        let commands =\n            vec![r#\"{\"id\":9527,\"method\":\"SUBSCRIBE\",\"params\":[\"!markPrice@arr\"]}\"#.to_string()];\n        ws_client.send(&commands).await;\n    } else {\n        let topics = symbols\n            .unwrap()\n            .iter()\n            .map(|symbol| (\"markPrice\".to_string(), symbol.to_string()))\n            .collect::<Vec<(String, String)>>();\n        ws_client.subscribe(&topics).await;\n    };\n\n    ws_client.run().await;\n    ws_client.close().await;\n}\n"
  },
  {
    "path": "crypto-crawler/src/crawlers/bitmex.rs",
    "content": "use super::{\n    crawl_candlestick_ext, crawl_event,\n    utils::{check_args, fetch_symbols_retry},\n};\nuse crate::{crawlers::utils::create_conversion_thread, msg::Message};\nuse crypto_market_type::MarketType;\nuse crypto_msg_type::MessageType;\nuse crypto_ws_client::*;\nuse std::sync::mpsc::Sender;\n\nconst EXCHANGE_NAME: &str = \"bitmex\";\n\nasync fn crawl_all(msg_type: MessageType, tx: Sender<Message>) {\n    let tx = create_conversion_thread(EXCHANGE_NAME.to_string(), msg_type, MarketType::Unknown, tx);\n\n    let channel: &str = match msg_type {\n        MessageType::Trade => \"trade\",\n        MessageType::L2Event => \"orderBookL2_25\",\n        MessageType::L2TopK => \"orderBook10\",\n        MessageType::BBO => \"quote\",\n        MessageType::L2Snapshot => \"orderBookL2\",\n        MessageType::FundingRate => \"funding\",\n        _ => panic!(\"unsupported message type {msg_type}\"),\n    };\n    let commands = vec![format!(r#\"{{\"op\":\"subscribe\",\"args\":[\"{channel}\"]}}\"#)];\n\n    let ws_client = BitmexWSClient::new(tx, None).await;\n    ws_client.send(&commands).await;\n    ws_client.run().await;\n    ws_client.close().await;\n}\n\npub(crate) async fn crawl_trade(\n    market_type: MarketType,\n    symbols: Option<&[String]>,\n    tx: Sender<Message>,\n) {\n    if market_type == MarketType::Unknown {\n        // crawl all symbols\n        crawl_all(MessageType::Trade, tx).await;\n    } else {\n        crawl_event(EXCHANGE_NAME, MessageType::Trade, market_type, symbols, tx).await;\n    }\n}\n\npub(crate) async fn crawl_l2_event(\n    market_type: MarketType,\n    symbols: Option<&[String]>,\n    tx: Sender<Message>,\n) {\n    if market_type == MarketType::Unknown {\n        // crawl all symbols\n        crawl_all(MessageType::L2Event, tx).await;\n    } else {\n        crawl_event(EXCHANGE_NAME, MessageType::L2Event, market_type, symbols, tx).await;\n    }\n}\n\npub(crate) async fn crawl_bbo(\n    market_type: MarketType,\n    symbols: Option<&[String]>,\n    tx: Sender<Message>,\n) {\n    if market_type == MarketType::Unknown {\n        // crawl all symbols\n        crawl_all(MessageType::BBO, tx).await;\n    } else {\n        crawl_event(EXCHANGE_NAME, MessageType::BBO, market_type, symbols, tx).await;\n    }\n}\n\npub(crate) async fn crawl_l2_topk(\n    market_type: MarketType,\n    symbols: Option<&[String]>,\n    tx: Sender<Message>,\n) {\n    if market_type == MarketType::Unknown {\n        // crawl all symbols\n        crawl_all(MessageType::L2TopK, tx).await;\n    } else {\n        crawl_event(EXCHANGE_NAME, MessageType::L2TopK, market_type, symbols, tx).await;\n    }\n}\n\n#[allow(clippy::unnecessary_unwrap)]\npub(crate) async fn crawl_funding_rate(\n    market_type: MarketType,\n    symbols: Option<&[String]>,\n    tx: Sender<Message>,\n) {\n    if market_type == MarketType::Unknown {\n        // crawl all symbols\n        crawl_all(MessageType::FundingRate, tx).await;\n    } else {\n        let is_empty = match symbols {\n            Some(list) => {\n                if list.is_empty() {\n                    true\n                } else {\n                    check_args(EXCHANGE_NAME, market_type, list);\n                    false\n                }\n            }\n            None => true,\n        };\n\n        let real_symbols = if is_empty {\n            tokio::task::block_in_place(move || fetch_symbols_retry(EXCHANGE_NAME, market_type))\n        } else {\n            symbols.unwrap().to_vec()\n        };\n        if real_symbols.is_empty() {\n            panic!(\"real_symbols is empty\");\n        }\n        let tx = create_conversion_thread(\n            EXCHANGE_NAME.to_string(),\n            MessageType::FundingRate,\n            market_type,\n            tx,\n        );\n\n        let topics: Vec<(String, String)> =\n            real_symbols.iter().map(|symbol| (\"funding\".to_string(), symbol.to_string())).collect();\n\n        match market_type {\n            MarketType::InverseSwap | MarketType::QuantoSwap => {\n                let ws_client = BitmexWSClient::new(tx, None).await;\n                ws_client.subscribe(&topics).await;\n                ws_client.run().await;\n                ws_client.close().await;\n            }\n            _ => panic!(\"BitMEX {market_type} does NOT have funding rates\"),\n        }\n    }\n}\n\npub(crate) async fn crawl_candlestick(\n    market_type: MarketType,\n    symbol_interval_list: Option<&[(String, usize)]>,\n    tx: Sender<Message>,\n) {\n    if market_type == MarketType::Unknown {\n        let tx = create_conversion_thread(\n            EXCHANGE_NAME.to_string(),\n            MessageType::Candlestick,\n            market_type,\n            tx,\n        );\n\n        let commands = vec![\n            r#\"{\"op\":\"subscribe\",\"args\":[\"tradeBin1m\"]}\"#.to_string(),\n            r#\"{\"op\":\"subscribe\",\"args\":[\"tradeBin5m\"]}\"#.to_string(),\n        ];\n\n        let ws_client = BitmexWSClient::new(tx, None).await;\n        ws_client.send(&commands).await;\n        ws_client.run().await;\n        ws_client.close().await;\n    } else {\n        crawl_candlestick_ext(EXCHANGE_NAME, market_type, symbol_interval_list, tx).await;\n    }\n}\n"
  },
  {
    "path": "crypto-crawler/src/crawlers/deribit.rs",
    "content": "use super::crawl_event;\nuse crate::{crawlers::utils::create_conversion_thread, msg::Message};\nuse crypto_market_type::MarketType;\nuse crypto_msg_type::MessageType;\nuse crypto_ws_client::*;\nuse std::sync::mpsc::Sender;\n\nconst EXCHANGE_NAME: &str = \"deribit\";\n\npub(crate) async fn crawl_trade(\n    market_type: MarketType,\n    symbols: Option<&[String]>,\n    tx: Sender<Message>,\n) {\n    if symbols.is_none() || symbols.unwrap().is_empty() {\n        let tx = create_conversion_thread(\n            EXCHANGE_NAME.to_string(),\n            MessageType::Trade,\n            market_type,\n            tx,\n        );\n\n        // \"any\" menas all, see https://docs.deribit.com/?javascript#trades-kind-currency-interval\n        let topics: Vec<(String, String)> = match market_type {\n            MarketType::InverseFuture => {\n                vec![(\"trades.future.SYMBOL.100ms\".to_string(), \"any\".to_string())]\n            }\n            MarketType::InverseSwap => {\n                vec![\n                    (\"trades.SYMBOL.100ms\".to_string(), \"BTC-PERPETUAL\".to_string()),\n                    (\"trades.SYMBOL.100ms\".to_string(), \"ETH-PERPETUAL\".to_string()),\n                ]\n            }\n            MarketType::EuropeanOption => {\n                vec![(\"trades.option.SYMBOL.100ms\".to_string(), \"any\".to_string())]\n            }\n            _ => panic!(\"Deribit does NOT have the {market_type} market type\"),\n        };\n\n        let ws_client = DeribitWSClient::new(tx, None).await;\n        ws_client.subscribe(&topics).await;\n        ws_client.run().await;\n        ws_client.close().await;\n    } else {\n        crawl_event(EXCHANGE_NAME, MessageType::Trade, market_type, symbols, tx).await;\n    }\n}\n"
  },
  {
    "path": "crypto-crawler/src/crawlers/huobi.rs",
    "content": "use super::utils::fetch_symbols_retry;\nuse crate::{\n    crawlers::{crawl_event, utils::create_conversion_thread},\n    msg::Message,\n};\nuse crypto_market_type::MarketType;\nuse crypto_msg_type::MessageType;\nuse crypto_ws_client::*;\nuse std::sync::mpsc::Sender;\n\nconst EXCHANGE_NAME: &str = \"huobi\";\n\n#[allow(clippy::unnecessary_unwrap)]\npub(crate) async fn crawl_l2_event(\n    market_type: MarketType,\n    symbols: Option<&[String]>,\n    tx: Sender<Message>,\n) {\n    match market_type {\n        MarketType::Spot => {\n            let tx = create_conversion_thread(\n                EXCHANGE_NAME.to_string(),\n                MessageType::L2Event,\n                market_type,\n                tx,\n            );\n            let symbols: Vec<String> = if symbols.is_none() || symbols.unwrap().is_empty() {\n                tokio::task::block_in_place(move || fetch_symbols_retry(EXCHANGE_NAME, market_type))\n            } else {\n                symbols.unwrap().to_vec()\n            };\n            // Huobi Spot market.$symbol.mbp.$levels must use wss://api.huobi.pro/feed\n            // or wss://api-aws.huobi.pro/feed\n            let ws_client = HuobiSpotWSClient::new(tx, Some(\"wss://api.huobi.pro/feed\")).await;\n            ws_client.subscribe_orderbook(&symbols).await;\n            ws_client.run().await;\n            ws_client.close().await;\n        }\n        MarketType::InverseFuture\n        | MarketType::LinearSwap\n        | MarketType::InverseSwap\n        | MarketType::EuropeanOption => {\n            crawl_event(EXCHANGE_NAME, MessageType::L2Event, market_type, symbols, tx).await\n        }\n        _ => panic!(\"Huobi does NOT have the {market_type} market type\"),\n    }\n}\n\n#[allow(clippy::unnecessary_unwrap)]\npub(crate) async fn crawl_funding_rate(\n    market_type: MarketType,\n    symbols: Option<&[String]>,\n    tx: Sender<Message>,\n) {\n    let tx = create_conversion_thread(\n        EXCHANGE_NAME.to_string(),\n        MessageType::FundingRate,\n        market_type,\n        tx,\n    );\n\n    let symbols: Vec<String> = if symbols.is_none() || symbols.unwrap().is_empty() {\n        vec![\"*\".to_string()]\n    } else {\n        symbols.unwrap().to_vec()\n    };\n    let commands: Vec<String> = symbols\n        .into_iter()\n        .map(|symbol| format!(r#\"{{\"topic\":\"public.{symbol}.funding_rate\",\"op\":\"sub\"}}\"#))\n        .collect();\n\n    match market_type {\n        MarketType::InverseSwap => {\n            let ws_client =\n                HuobiInverseSwapWSClient::new(tx, Some(\"wss://api.hbdm.com/swap-notification\"))\n                    .await;\n            ws_client.send(&commands).await;\n            ws_client.run().await;\n            ws_client.close().await;\n        }\n        MarketType::LinearSwap => {\n            let ws_client = HuobiLinearSwapWSClient::new(\n                tx,\n                Some(\"wss://api.hbdm.com/linear-swap-notification\"),\n            )\n            .await;\n            ws_client.send(&commands).await;\n            ws_client.run().await;\n            ws_client.close().await;\n        }\n        _ => panic!(\"Huobi {market_type} does NOT have funding rates\"),\n    }\n}\n"
  },
  {
    "path": "crypto-crawler/src/crawlers/kucoin.rs",
    "content": "use crate::{crawlers::utils::create_conversion_thread, msg::Message};\nuse crypto_market_type::MarketType;\nuse crypto_msg_type::MessageType;\nuse crypto_ws_client::*;\nuse std::sync::mpsc::Sender;\n\nuse super::crawl_event;\n\nconst EXCHANGE_NAME: &str = \"kucoin\";\n\npub(crate) async fn crawl_bbo(\n    market_type: MarketType,\n    symbols: Option<&[String]>,\n    tx: Sender<Message>,\n) {\n    if market_type == MarketType::Spot && (symbols.is_none() || symbols.unwrap().is_empty()) {\n        let tx =\n            create_conversion_thread(EXCHANGE_NAME.to_string(), MessageType::BBO, market_type, tx);\n\n        // https://docs.kucoin.com/#all-symbols-ticker\n        let commands: Vec<String> = vec![r#\"{\"id\":\"crypto-ws-client\",\"type\":\"subscribe\",\"topic\":\"/market/ticker:all\",\"privateChannel\":false,\"response\":true}\"#.to_string()];\n        let ws_client = KuCoinSpotWSClient::new(tx, None).await;\n        ws_client.send(&commands).await;\n        ws_client.run().await;\n        ws_client.close().await;\n    } else {\n        crawl_event(EXCHANGE_NAME, MessageType::BBO, market_type, symbols, tx).await;\n    }\n}\n"
  },
  {
    "path": "crypto-crawler/src/crawlers/mod.rs",
    "content": "#[macro_use]\nmod utils;\n\npub(super) mod binance;\npub(super) mod bitmex;\npub(super) mod deribit;\npub(super) mod huobi;\npub(super) mod kucoin;\npub(super) mod okx;\npub(super) mod zb;\npub(super) mod zbg;\n\npub use utils::fetch_symbols_retry;\npub(super) use utils::{\n    crawl_candlestick_ext, crawl_event, crawl_open_interest, crawl_snapshot,\n    create_ws_client_symbol,\n};\n"
  },
  {
    "path": "crypto-crawler/src/crawlers/okx.rs",
    "content": "use super::utils::fetch_symbols_retry;\nuse crate::{crawlers::utils::create_conversion_thread, msg::Message};\nuse crypto_market_type::MarketType;\nuse crypto_msg_type::MessageType;\nuse crypto_ws_client::*;\nuse std::sync::mpsc::Sender;\n\nconst EXCHANGE_NAME: &str = \"okx\";\n\n#[allow(clippy::unnecessary_unwrap)]\npub(crate) async fn crawl_funding_rate(\n    market_type: MarketType,\n    symbols: Option<&[String]>,\n    tx: Sender<Message>,\n) {\n    let tx = create_conversion_thread(\n        EXCHANGE_NAME.to_string(),\n        MessageType::FundingRate,\n        market_type,\n        tx,\n    );\n\n    let symbols: Vec<String> = if symbols.is_none() || symbols.unwrap().is_empty() {\n        tokio::task::block_in_place(move || fetch_symbols_retry(EXCHANGE_NAME, market_type))\n    } else {\n        symbols.unwrap().to_vec()\n    };\n    let topics: Vec<(String, String)> =\n        symbols.into_iter().map(|symbol| (\"funding-rate\".to_string(), symbol)).collect();\n\n    match market_type {\n        MarketType::InverseSwap | MarketType::LinearSwap => {\n            let ws_client = OkxWSClient::new(tx, None).await;\n            ws_client.subscribe(&topics).await;\n            ws_client.run().await;\n            ws_client.close().await;\n        }\n        _ => panic!(\"OKX {market_type} does NOT have funding rates\"),\n    }\n}\n\n#[deprecated(since = \"4.1.2\", note = \"OKX open interest is fetched via HTTP for now\")]\n#[allow(dead_code)]\npub(crate) async fn crawl_open_interest(\n    market_type: MarketType,\n    symbols: Option<&[String]>,\n    tx: Sender<Message>,\n) {\n    let tx = create_conversion_thread(\n        EXCHANGE_NAME.to_string(),\n        MessageType::OpenInterest,\n        market_type,\n        tx,\n    );\n\n    let symbols = if let Some(symbols) = symbols {\n        if symbols.is_empty() {\n            tokio::task::block_in_place(move || fetch_symbols_retry(EXCHANGE_NAME, market_type))\n        } else {\n            symbols.to_vec()\n        }\n    } else {\n        tokio::task::block_in_place(move || fetch_symbols_retry(EXCHANGE_NAME, market_type))\n    };\n    let topics: Vec<(String, String)> =\n        symbols.into_iter().map(|symbol| (\"open-interest\".to_string(), symbol)).collect();\n\n    if market_type != MarketType::Spot {\n        let ws_client = OkxWSClient::new(tx, None).await;\n        ws_client.subscribe(&topics).await;\n        ws_client.run().await;\n        ws_client.close().await;\n    } else {\n        panic!(\"spot does NOT have open interest\");\n    }\n}\n"
  },
  {
    "path": "crypto-crawler/src/crawlers/utils.rs",
    "content": "use std::{\n    sync::{mpsc::Sender, Arc},\n    time::{Duration, SystemTime, UNIX_EPOCH},\n};\n\nuse crate::utils::{REST_LOCKS, WS_LOCKS};\nuse crypto_market_type::{get_market_types, MarketType};\nuse crypto_markets::fetch_symbols;\nuse crypto_rest_client::{fetch_l2_snapshot, fetch_l3_snapshot, fetch_open_interest};\nuse crypto_ws_client::*;\nuse log::*;\n\nuse crate::{get_hot_spot_symbols, utils::cmc_rank::sort_by_cmc_rank, Message, MessageType};\n\npub fn fetch_symbols_retry(exchange: &str, market_type: MarketType) -> Vec<String> {\n    let retry_count = std::env::var(\"REST_RETRY_COUNT\")\n        .unwrap_or_else(|_| \"5\".to_string())\n        .parse::<i64>()\n        .unwrap();\n    let cooldown_time = get_cooldown_time_per_request(exchange, market_type);\n    let lock = REST_LOCKS.get(exchange).unwrap().get(&market_type).unwrap().clone();\n    let mut symbols = Vec::<String>::new();\n    let mut backoff_factor = 1;\n    for i in 0..retry_count {\n        let mut lock_ = lock.lock().unwrap();\n        if !lock_.owns_lock() {\n            lock_.lock().unwrap();\n        }\n        match fetch_symbols(exchange, market_type) {\n            Ok(list) => {\n                symbols = list;\n                if lock_.owns_lock() {\n                    lock_.unlock().unwrap();\n                }\n                break;\n            }\n            Err(err) => {\n                backoff_factor *= 2;\n                if i == retry_count - 1 {\n                    error!(\"The {}th time, {}\", i, err);\n                } else {\n                    warn!(\"The {}th time, {}\", i, err);\n                }\n            }\n        }\n        // Cooldown after each request, and make all other processes wait\n        // on the lock to avoid parallel requests, thus avoid 429 error\n        std::thread::sleep(cooldown_time * backoff_factor);\n        if lock_.owns_lock() {\n            lock_.unlock().unwrap();\n        }\n    }\n    symbols\n}\n\npub(super) fn check_args(exchange: &str, market_type: MarketType, symbols: &[String]) {\n    let market_types = get_market_types(exchange);\n    if !market_types.contains(&market_type) {\n        panic!(\"{exchange} does NOT have the {market_type} market type\");\n    }\n\n    let valid_symbols = fetch_symbols_retry(exchange, market_type);\n    let invalid_symbols: Vec<String> =\n        symbols.iter().filter(|symbol| !valid_symbols.contains(symbol)).cloned().collect();\n    if !invalid_symbols.is_empty() {\n        panic!(\n            \"Invalid symbols: {}, {} {} available trading symbols are {}\",\n            invalid_symbols.join(\",\"),\n            exchange,\n            market_type,\n            valid_symbols.join(\",\")\n        );\n    }\n}\n\nfn get_cooldown_time_per_request(exchange: &str, market_type: MarketType) -> Duration {\n    let millis = match exchange {\n        \"binance\" => 500,    // spot weitht 1200, contract weight 2400\n        \"bitget\" => 100,     // 20 requests per 2 seconds\n        \"bithumb\" => 8 * 10, /* 135 requests per 1 second for public APIs, multiplied by 10 to */\n        // reduce its frequency\n        \"bitmex\" => 2000, /* 60 requests per minute on all routes (reduced to 30 when */\n        // unauthenticated)\n        \"bitstamp\" => 75 * 10, /* 8000 requests per 10 minutes, but bitstamp orderbook is too */\n        // big, need to reduce its frequency\n        \"bitz\" => 34,       // no more than 30 times within 1 second\n        \"bybit\" => 20 * 10, /* 50 requests per second continuously for 2 minutes, multiplied */\n        // by 10 to reduce its frequency\n        \"coinbase_pro\" => 100, //  10 requests per second\n        \"deribit\" => 50,       // 20 requests per second\n        \"dydx\" => 100,         // 100 requests per 10 seconds\n        \"gate\" => 4,           // 300 read operations per IP per second\n        \"huobi\" => 2,          // 800 times/second for one IP\n        \"kucoin\" => match market_type {\n            MarketType::Spot => 300, // 3x to avoid 429\n            _ => 100,                // 30 times/3s\n        },\n        \"mexc\" => 100, // 20 times per 2 seconds\n        \"okx\" => 100,  // 20 requests per 2 seconds\n        _ => 100,\n    };\n    Duration::from_millis(millis)\n}\n\n/// Crawl leve2 or level3 orderbook snapshots through RESTful APIs.\npub(crate) fn crawl_snapshot(\n    exchange: &str,\n    market_type: MarketType,\n    msg_type: MessageType, // L2Snapshot or L3Snapshot\n    symbols: Option<&[String]>,\n    tx: Sender<Message>,\n) {\n    let is_empty = match symbols {\n        Some(list) => {\n            if list.is_empty() {\n                true\n            } else {\n                check_args(exchange, market_type, list);\n                false\n            }\n        }\n        None => true,\n    };\n\n    let cooldown_time = get_cooldown_time_per_request(exchange, market_type);\n\n    let lock = REST_LOCKS.get(exchange).unwrap().get(&market_type).unwrap().clone();\n    'outer: loop {\n        let mut real_symbols = if is_empty {\n            if market_type == MarketType::Spot {\n                let spot_symbols = fetch_symbols_retry(exchange, market_type);\n                get_hot_spot_symbols(exchange, &spot_symbols)\n            } else {\n                fetch_symbols_retry(exchange, market_type)\n            }\n        } else {\n            symbols.unwrap().to_vec()\n        };\n        sort_by_cmc_rank(exchange, &mut real_symbols);\n\n        let mut index = 0_usize;\n        let mut success_count = 0_u64;\n        let mut backoff_factor = 1;\n        // retry 5 times at most\n        while index < real_symbols.len() && backoff_factor < 6 {\n            let symbol = real_symbols[index].as_str();\n            let mut lock_ = lock.lock().unwrap();\n            if !lock_.owns_lock() {\n                lock_.lock().unwrap();\n            }\n            let resp = match msg_type {\n                MessageType::L2Snapshot => fetch_l2_snapshot(exchange, market_type, symbol, None),\n                MessageType::L3Snapshot => fetch_l3_snapshot(exchange, market_type, symbol, None),\n                _ => panic!(\"msg_type must be L2Snapshot or L3Snapshot\"),\n            };\n            // Cooldown after each request, and make all other processes wait\n            // on the lock to avoid parallel requests, thus avoid 429 error\n            std::thread::sleep(cooldown_time);\n            if lock_.owns_lock() {\n                lock_.unlock().unwrap();\n            }\n            match resp {\n                Ok(msg) => {\n                    index += 1;\n                    success_count += 1;\n                    backoff_factor = 1;\n                    let message = Message::new_with_symbol(\n                        exchange.to_string(),\n                        market_type,\n                        msg_type,\n                        symbol.to_string(),\n                        msg,\n                    );\n                    if tx.send(message).is_err() {\n                        // break the loop if there is no receiver\n                        break 'outer;\n                    }\n                }\n                Err(err) => {\n                    let current_timestamp = SystemTime::now()\n                        .duration_since(SystemTime::UNIX_EPOCH)\n                        .unwrap()\n                        .as_millis() as u64;\n                    warn!(\n                        \"{} {} {} {} {} {}, error: {}, back off for {} milliseconds\",\n                        current_timestamp,\n                        success_count,\n                        backoff_factor,\n                        exchange,\n                        market_type,\n                        symbol,\n                        err,\n                        (backoff_factor * cooldown_time).as_millis()\n                    );\n                    std::thread::sleep(backoff_factor * cooldown_time);\n                    success_count = 0;\n                    backoff_factor += 1;\n                }\n            }\n        }\n        std::thread::sleep(cooldown_time * 2); // if real_symbols is empty, CPU will be 100% without this line\n    }\n}\n\n/// Crawl open interests of all trading symbols.\npub(crate) fn crawl_open_interest(exchange: &str, market_type: MarketType, tx: Sender<Message>) {\n    let cooldown_time = get_cooldown_time_per_request(exchange, market_type);\n\n    let lock = REST_LOCKS.get(exchange).unwrap().get(&market_type).unwrap().clone();\n    'outer: loop {\n        match exchange {\n            \"bitz\" | \"deribit\" | \"dydx\" | \"ftx\" | \"huobi\" | \"kucoin\" | \"okx\" => {\n                let mut lock_ = lock.lock().unwrap();\n                if !lock_.owns_lock() {\n                    lock_.lock().unwrap();\n                }\n                let resp = fetch_open_interest(exchange, market_type, None);\n                if let Ok(json) = resp {\n                    if exchange == \"deribit\" {\n                        // A RESTful response of deribit open_interest contains four lines\n                        for x in json.trim().split('\\n') {\n                            let message = Message::new(\n                                exchange.to_string(),\n                                market_type,\n                                MessageType::OpenInterest,\n                                x.to_string(),\n                            );\n                            if tx.send(message).is_err() {\n                                break; // break the loop if there is no receiver\n                            }\n                        }\n                    } else {\n                        let message = Message::new(\n                            exchange.to_string(),\n                            market_type,\n                            MessageType::OpenInterest,\n                            json,\n                        );\n                        if tx.send(message).is_err() {\n                            break; // break the loop if there is no receiver\n                        }\n                    }\n                }\n                // Cooldown after each request, and make all other processes wait\n                // on the lock to avoid parallel requests, thus avoid 429 error\n                std::thread::sleep(cooldown_time);\n                if lock_.owns_lock() {\n                    lock_.unlock().unwrap();\n                }\n            }\n            \"binance\" | \"bitget\" | \"bybit\" | \"gate\" | \"zbg\" => {\n                let real_symbols = fetch_symbols_retry(exchange, market_type);\n\n                let mut index = 0_usize;\n                let mut success_count = 0_u64;\n                let mut backoff_factor = 1;\n                // retry 5 times at most\n                while index < real_symbols.len() && backoff_factor < 6 {\n                    let symbol = real_symbols[index].as_str();\n                    let mut lock_ = lock.lock().unwrap();\n                    if !lock_.owns_lock() {\n                        lock_.lock().unwrap();\n                    }\n                    let resp = fetch_open_interest(exchange, market_type, Some(symbol));\n                    // Cooldown after each request, and make all other processes wait\n                    // on the lock to avoid parallel requests, thus avoid 429 error\n                    std::thread::sleep(cooldown_time);\n                    if lock_.owns_lock() {\n                        lock_.unlock().unwrap();\n                    }\n                    match resp {\n                        Ok(msg) => {\n                            index += 1;\n                            success_count += 1;\n                            backoff_factor = 1;\n                            let message = Message::new_with_symbol(\n                                exchange.to_string(),\n                                market_type,\n                                MessageType::OpenInterest,\n                                symbol.to_string(),\n                                msg,\n                            );\n                            if tx.send(message).is_err() {\n                                // break the loop if there is no receiver\n                                break 'outer;\n                            }\n                        }\n                        Err(err) => {\n                            let current_timestamp = SystemTime::now()\n                                .duration_since(SystemTime::UNIX_EPOCH)\n                                .unwrap()\n                                .as_millis()\n                                as u64;\n                            warn!(\n                                \"{} {} {} {} {} {}, error: {}, back off for {} milliseconds\",\n                                current_timestamp,\n                                success_count,\n                                backoff_factor,\n                                exchange,\n                                market_type,\n                                symbol,\n                                err,\n                                (backoff_factor * cooldown_time).as_millis()\n                            );\n                            std::thread::sleep(backoff_factor * cooldown_time);\n                            success_count = 0;\n                            backoff_factor += 1;\n                        }\n                    }\n                }\n            }\n            _ => panic!(\"{exchange} does NOT have open interest RESTful API\"),\n        }\n        std::thread::sleep(cooldown_time * 2); // if real_symbols is empty, CPU will be 100% without this line\n    }\n}\n\nasync fn subscribe_with_lock(\n    exchange: String,\n    market_type: MarketType,\n    msg_type: MessageType,\n    symbols: Vec<String>,\n    ws_client: Arc<dyn WSClient + Send + Sync>,\n) {\n    match msg_type {\n        MessageType::BBO => ws_client.subscribe_bbo(&symbols).await,\n        MessageType::Trade => ws_client.subscribe_trade(&symbols).await,\n        MessageType::L2Event => ws_client.subscribe_orderbook(&symbols).await,\n        MessageType::L3Event => ws_client.subscribe_l3_orderbook(&symbols).await,\n        MessageType::L2TopK => ws_client.subscribe_orderbook_topk(&symbols).await,\n        MessageType::Ticker => ws_client.subscribe_ticker(&symbols).await,\n        _ => panic!(\"{exchange} {market_type} does NOT have {msg_type} websocket channel\"),\n    };\n}\n\nfn get_connection_interval_ms(exchange: &str, _market_type: MarketType) -> Option<u64> {\n    match exchange {\n        \"bitfinex\" => Some(3000), /* you cannot open more than 20 connections per minute, see https://docs.bitfinex.com/docs/requirements-and-limitations#websocket-rate-limits */\n        // \"bitmex\" => Some(9000), // 40 per hour\n        \"bitz\" => Some(100), /* `cat crawler-trade-bitz-spot-error-12.log` has many \"429 Too */\n        // Many Requests\"\n        \"kucoin\" => Some(2000), /* Connection Limit: 30 per minute, see https://docs.kucoin.com/#connection-times */\n        \"okx\" => Some(1000), /* Connection limit: 1 time per second, https://www.okx.com/docs-v5/en/#websocket-api-connect */\n        _ => None,\n    }\n}\n\nfn get_num_subscriptions_per_connection(exchange: &str, market_type: MarketType) -> usize {\n    match exchange {\n        // A single connection can listen to a maximum of 200 streams\n        \"binance\" => {\n            if market_type == MarketType::Spot {\n                // https://binance-docs.github.io/apidocs/spot/en/#websocket-limits\n                1024\n            } else {\n                // https://binance-docs.github.io/apidocs/futures/en/#websocket-market-streams\n                // https://binance-docs.github.io/apidocs/delivery/en/#websocket-market-streams\n                200\n            }\n        } // https://binance-docs.github.io/apidocs/futures/en/#websocket-market-streams\n        // All websocket connections have a limit of 30 subscriptions to public market data feed\n        // channels\n        \"bitfinex\" => 30, // https://docs.bitfinex.com/docs/ws-general#subscribe-to-channels\n        \"kucoin\" => 300, /* Subscription limit for each connection: 300 topics, see https://docs.kucoin.com/#topic-subscription-limit */\n        \"okx\" => 256,    // okx spot l2_event throws many ResetWithoutClosingHandshake errors\n        _ => usize::MAX, // usize::MAX means unlimited\n    }\n}\n\nasync fn create_ws_client_internal(\n    exchange: &str,\n    market_type: MarketType,\n    tx: Sender<String>,\n) -> Arc<dyn WSClient + Send + Sync> {\n    match exchange {\n        \"binance\" => match market_type {\n            MarketType::Spot => Arc::new(BinanceSpotWSClient::new(tx, None).await),\n            MarketType::InverseFuture | MarketType::InverseSwap => {\n                Arc::new(BinanceInverseWSClient::new(tx, None).await)\n            }\n            MarketType::LinearFuture | MarketType::LinearSwap => {\n                Arc::new(BinanceLinearWSClient::new(tx, None).await)\n            }\n            MarketType::EuropeanOption => Arc::new(BinanceOptionWSClient::new(tx, None).await),\n            _ => panic!(\"Binance does NOT have the {market_type} market type\"),\n        },\n        \"bitfinex\" => Arc::new(BitfinexWSClient::new(tx, None).await),\n        \"bitget\" => match market_type {\n            MarketType::Spot => Arc::new(BitgetSpotWSClient::new(tx, None).await),\n            MarketType::InverseFuture | MarketType::InverseSwap | MarketType::LinearSwap => {\n                Arc::new(BitgetSwapWSClient::new(tx, None).await)\n            }\n            _ => panic!(\"Bitget does NOT have the {market_type} market type\"),\n        },\n        \"bithumb\" => Arc::new(BithumbWSClient::new(tx, None).await),\n        \"bitmex\" => Arc::new(BitmexWSClient::new(tx, None).await),\n        \"bitstamp\" => Arc::new(BitstampWSClient::new(tx, None).await),\n        \"bitz\" => match market_type {\n            MarketType::Spot => Arc::new(BitzSpotWSClient::new(tx, None).await),\n            _ => panic!(\"Bitz does NOT have the {market_type} market type\"),\n        },\n        \"bybit\" => match market_type {\n            MarketType::InverseFuture | MarketType::InverseSwap => {\n                Arc::new(BybitInverseWSClient::new(tx, None).await)\n            }\n            MarketType::LinearSwap => Arc::new(BybitLinearSwapWSClient::new(tx, None).await),\n            _ => panic!(\"Bybit does NOT have the {market_type} market type\"),\n        },\n        \"coinbase_pro\" => Arc::new(CoinbaseProWSClient::new(tx, None).await),\n        \"deribit\" => Arc::new(DeribitWSClient::new(tx, None).await),\n        \"dydx\" => match market_type {\n            MarketType::LinearSwap => Arc::new(DydxSwapWSClient::new(tx, None).await),\n            _ => panic!(\"dYdX does NOT have the {market_type} market type\"),\n        },\n        \"ftx\" => Arc::new(FtxWSClient::new(tx, None).await),\n        \"gate\" => match market_type {\n            MarketType::Spot => Arc::new(GateSpotWSClient::new(tx, None).await),\n            MarketType::InverseSwap => Arc::new(GateInverseSwapWSClient::new(tx, None).await),\n            MarketType::LinearSwap => Arc::new(GateLinearSwapWSClient::new(tx, None).await),\n            MarketType::InverseFuture => Arc::new(GateInverseFutureWSClient::new(tx, None).await),\n            MarketType::LinearFuture => Arc::new(GateLinearFutureWSClient::new(tx, None).await),\n            _ => panic!(\"Gate does NOT have the {market_type} market type\"),\n        },\n        \"huobi\" => match market_type {\n            MarketType::Spot => Arc::new(HuobiSpotWSClient::new(tx, None).await),\n            MarketType::InverseFuture => Arc::new(HuobiFutureWSClient::new(tx, None).await),\n            MarketType::LinearSwap => Arc::new(HuobiLinearSwapWSClient::new(tx, None).await),\n            MarketType::InverseSwap => Arc::new(HuobiInverseSwapWSClient::new(tx, None).await),\n            MarketType::EuropeanOption => Arc::new(HuobiOptionWSClient::new(tx, None).await),\n            _ => panic!(\"Huobi does NOT have the {market_type} market type\"),\n        },\n        \"kraken\" => match market_type {\n            MarketType::Spot => Arc::new(KrakenSpotWSClient::new(tx, None).await),\n            MarketType::InverseFuture | MarketType::InverseSwap => {\n                Arc::new(KrakenFuturesWSClient::new(tx, None).await)\n            }\n            _ => panic!(\"Kraken does NOT have the {market_type} market type\"),\n        },\n        \"kucoin\" => match market_type {\n            MarketType::Spot => Arc::new(KuCoinSpotWSClient::new(tx, None).await),\n            MarketType::InverseSwap | MarketType::LinearSwap | MarketType::InverseFuture => {\n                Arc::new(KuCoinSwapWSClient::new(tx, None).await)\n            }\n            _ => panic!(\"KuCoin does NOT have the {market_type} market type\"),\n        },\n        \"mexc\" => match market_type {\n            MarketType::Spot => Arc::new(MexcSpotWSClient::new(tx, None).await),\n            MarketType::LinearSwap | MarketType::InverseSwap => {\n                Arc::new(MexcSwapWSClient::new(tx, None).await)\n            }\n            _ => panic!(\"MEXC does NOT have the {market_type} market type\"),\n        },\n        \"okx\" => Arc::new(OkxWSClient::new(tx, None).await),\n        \"zb\" => match market_type {\n            MarketType::Spot => Arc::new(ZbSpotWSClient::new(tx, None).await),\n            MarketType::LinearSwap => Arc::new(ZbSwapWSClient::new(tx, None).await),\n            _ => panic!(\"ZB does NOT have the {market_type} market type\"),\n        },\n        \"zbg\" => match market_type {\n            MarketType::Spot => Arc::new(ZbgSpotWSClient::new(tx, None).await),\n            MarketType::InverseSwap | MarketType::LinearSwap => {\n                Arc::new(ZbgSwapWSClient::new(tx, None).await)\n            }\n            _ => panic!(\"ZBG does NOT have the {market_type} market type\"),\n        },\n        _ => panic!(\"Unknown exchange {exchange}\"),\n    }\n}\n\nasync fn create_ws_client(\n    exchange: &str,\n    market_type: MarketType,\n    msg_type: MessageType,\n    tx: Sender<Message>,\n) -> Arc<dyn WSClient + Send + Sync> {\n    let tx = create_conversion_thread(exchange.to_string(), msg_type, market_type, tx);\n    if let Some(interval) = get_connection_interval_ms(exchange, market_type) {\n        let lock = WS_LOCKS.get(exchange).unwrap().get(&market_type).unwrap().clone();\n        let mut lock = lock.lock().await;\n        let mut i = 0;\n        while !lock.owns_lock() {\n            i += 1;\n            debug!(\n                \"{} {} {} try_lock_with_pid() the {}th time\",\n                exchange, market_type, msg_type, i\n            );\n            if lock.try_lock_with_pid().is_ok() {\n                break;\n            } else {\n                tokio::time::sleep(std::time::Duration::from_millis(\n                    rand::random::<u64>() % 90 + 11,\n                ))\n                .await; // give chances to other tasks\n            }\n        }\n        let ws_client = create_ws_client_internal(exchange, market_type, tx).await;\n        tokio::time::sleep(Duration::from_millis(interval)).await;\n        if lock.owns_lock() {\n            lock.unlock().unwrap();\n        }\n        ws_client\n    } else {\n        create_ws_client_internal(exchange, market_type, tx).await\n    }\n}\n\npub(crate) async fn create_ws_client_symbol(\n    exchange: &str,\n    market_type: MarketType,\n    tx: Sender<String>,\n) -> Arc<dyn WSClient + Send + Sync> {\n    let tx = create_parser_thread(exchange.to_string(), market_type, tx);\n    create_ws_client_internal(exchange, market_type, tx).await\n}\n\n#[derive(Clone)]\nstruct EmptyStruct {} // for stop channel\n\nfn create_symbol_discovery_thread(\n    exchange: String,\n    market_type: MarketType,\n    subscribed_symbols: Vec<String>,\n    mut stop_ch_rx: tokio::sync::broadcast::Receiver<EmptyStruct>,\n    tx: tokio::sync::mpsc::Sender<Vec<String>>, // send out new symbols\n) -> tokio::task::JoinHandle<()> {\n    let num_topics_per_connection = get_num_subscriptions_per_connection(&exchange, market_type);\n    let mut subscribed_symbols = subscribed_symbols;\n    let mut num_subscribed_of_last_client = subscribed_symbols.len() % num_topics_per_connection;\n    let mut hourly = tokio::time::interval(Duration::from_secs(3600));\n    tokio::task::spawn(async move {\n        loop {\n            tokio::select! {\n                _ = stop_ch_rx.recv() => {\n                    break;\n                }\n                _ = hourly.tick() => {\n                    let exchange_clone = exchange.to_string();\n                    let latest_symbols = tokio::task::block_in_place(move || {\n                        fetch_symbols_retry(&exchange_clone, market_type)\n                    });\n\n                    let mut new_symbols: Vec<String> = latest_symbols\n                        .iter()\n                        .filter(|s| !subscribed_symbols.contains(s))\n                        .cloned()\n                        .collect();\n\n                    if !new_symbols.is_empty() {\n                        warn!(\"Found new symbols: {}\", new_symbols.join(\", \"));\n                        if tx.send(new_symbols.clone()).await.is_err() {\n                            break; // break the loop if there is no receiver\n                        }\n                        num_subscribed_of_last_client += new_symbols.len();\n                        subscribed_symbols.append(&mut new_symbols);\n                    }\n                    if num_subscribed_of_last_client >= num_topics_per_connection {\n                        panic!(\n                            \"The last connection has subscribed {num_subscribed_of_last_client} topics, which is more than {num_topics_per_connection}, restarting the process\",\n                        ); // pm2 will restart the whole process\n                    }\n                }\n            }\n        }\n    })\n}\n\nfn create_new_symbol_receiver_thread(\n    exchange: String,\n    msg_type: MessageType,\n    market_type: MarketType,\n    mut symbols_rx: tokio::sync::mpsc::Receiver<Vec<String>>,\n    ws_client: Arc<dyn WSClient + Send + Sync>,\n) -> tokio::task::JoinHandle<()> {\n    tokio::task::spawn(async move {\n        let exchange_clone = exchange;\n        while let Some(new_symbols) = symbols_rx.recv().await {\n            subscribe_with_lock(\n                exchange_clone.clone(),\n                market_type,\n                msg_type,\n                new_symbols,\n                ws_client.clone(),\n            )\n            .await;\n        }\n    })\n}\n\nfn create_new_symbol_receiver_thread_candlestick(\n    intervals: Vec<usize>,\n    mut rx: tokio::sync::mpsc::Receiver<Vec<String>>,\n    ws_client: Arc<dyn WSClient + Send + Sync>,\n) -> tokio::task::JoinHandle<()> {\n    tokio::task::spawn(async move {\n        while let Some(new_symbols) = rx.recv().await {\n            let new_symbol_interval_list = new_symbols\n                .iter()\n                .flat_map(|symbol| {\n                    intervals.clone().into_iter().map(move |interval| (symbol.clone(), interval))\n                })\n                .collect::<Vec<(String, usize)>>();\n            ws_client.subscribe_candlestick(&new_symbol_interval_list).await;\n        }\n    })\n}\n\n// create a thread to convert Sender<Message> Sender<String>\npub(crate) fn create_conversion_thread(\n    exchange: String,\n    msg_type: MessageType,\n    market_type: MarketType,\n    tx: Sender<Message>,\n) -> Sender<String> {\n    let (tx_raw, rx_raw) = std::sync::mpsc::channel();\n    tokio::task::spawn_blocking(move || {\n        for json in rx_raw {\n            let msg = Message::new(exchange.clone(), market_type, msg_type, json);\n            if tx.send(msg).is_err() {\n                break; // break the loop if there is no receiver\n            }\n        }\n    });\n    tx_raw\n}\n\n// create a thread to call `crypto-msg-parser`\nfn create_parser_thread(\n    exchange: String,\n    market_type: MarketType,\n    tx: Sender<String>,\n) -> Sender<String> {\n    let (tx_raw, rx_raw) = std::sync::mpsc::channel::<String>();\n    std::thread::spawn(move || {\n        for json in rx_raw {\n            let msg_type = crypto_msg_parser::get_msg_type(&exchange, &json);\n            let parsed = match msg_type {\n                MessageType::Trade => serde_json::to_string(\n                    &crypto_msg_parser::parse_trade(&exchange, market_type, &json).unwrap(),\n                )\n                .unwrap(),\n                MessageType::L2Event => {\n                    let received_at = SystemTime::now()\n                        .duration_since(UNIX_EPOCH)\n                        .unwrap()\n                        .as_millis()\n                        .try_into()\n                        .unwrap();\n                    serde_json::to_string(\n                        &crypto_msg_parser::parse_l2(\n                            &exchange,\n                            market_type,\n                            &json,\n                            Some(received_at),\n                        )\n                        .unwrap(),\n                    )\n                    .unwrap()\n                }\n                _ => panic!(\"unknown msg type {msg_type}\"),\n            };\n            if tx.send(parsed).is_err() {\n                break; // break the loop if there is no receiver\n            }\n        }\n    });\n    tx_raw\n}\n\nasync fn crawl_event_one_chunk(\n    exchange: String,\n    market_type: MarketType,\n    msg_type: MessageType,\n    ws_client: Option<Arc<dyn WSClient + Send + Sync>>,\n    symbols: Vec<String>,\n    tx: Sender<Message>,\n) -> tokio::task::JoinHandle<()> {\n    let ws_client = if let Some(ws_client) = ws_client {\n        ws_client\n    } else {\n        let tx_clone = tx.clone();\n        create_ws_client(&exchange, market_type, msg_type, tx_clone).await\n    };\n\n    {\n        // fire and forget\n        let exchange_clone = exchange.to_string();\n        let ws_client_clone = ws_client.clone();\n        tokio::task::spawn(async move {\n            subscribe_with_lock(exchange_clone, market_type, msg_type, symbols, ws_client_clone)\n                .await;\n        });\n    }\n\n    tokio::task::spawn(async move {\n        ws_client.run().await;\n        ws_client.close().await;\n    })\n}\n\npub(crate) async fn crawl_event(\n    exchange: &str,\n    msg_type: MessageType,\n    market_type: MarketType,\n    symbols: Option<&[String]>,\n    tx: Sender<Message>,\n) {\n    let num_topics_per_connection = get_num_subscriptions_per_connection(exchange, market_type);\n    let is_empty = match symbols {\n        Some(list) => {\n            if list.is_empty() {\n                true\n            } else {\n                tokio::task::block_in_place(move || check_args(exchange, market_type, list));\n                false\n            }\n        }\n        None => true,\n    };\n    let automatic_symbol_discovery = is_empty;\n\n    let real_symbols = if is_empty {\n        tokio::task::block_in_place(move || fetch_symbols_retry(exchange, market_type))\n    } else {\n        symbols.unwrap().to_vec()\n    };\n    if real_symbols.is_empty() {\n        error!(\"real_symbols is empty due to fetch_symbols_retry() failure\");\n        return;\n    }\n\n    // The stop channel is used by all tokio tasks\n    let (stop_ch_tx, stop_ch_rx) = tokio::sync::broadcast::channel::<EmptyStruct>(1);\n\n    // create a thread to discover new symbols\n    let (tx_symbols, rx_symbols) = tokio::sync::mpsc::channel::<Vec<String>>(4);\n    let symbol_discovery_thread = if automatic_symbol_discovery {\n        let thread = create_symbol_discovery_thread(\n            exchange.to_string(),\n            market_type,\n            real_symbols.clone(),\n            stop_ch_rx,\n            tx_symbols,\n        );\n        Some(thread)\n    } else {\n        None\n    };\n\n    // create a thread to convert Sender<String> to Sender<Message>\n    if real_symbols.len() <= num_topics_per_connection {\n        let ws_client = create_ws_client(exchange, market_type, msg_type, tx).await;\n        subscribe_with_lock(\n            exchange.to_string(),\n            market_type,\n            msg_type,\n            real_symbols,\n            ws_client.clone(),\n        )\n        .await;\n        if automatic_symbol_discovery {\n            create_new_symbol_receiver_thread(\n                exchange.to_string(),\n                msg_type,\n                market_type,\n                rx_symbols,\n                ws_client.clone(),\n            );\n        }\n        ws_client.run().await;\n        ws_client.close().await;\n    } else {\n        // split to chunks\n        let mut chunks: Vec<Vec<String>> = Vec::new();\n        for i in (0..real_symbols.len()).step_by(num_topics_per_connection) {\n            let chunk = real_symbols\n                [i..(std::cmp::min(i + num_topics_per_connection, real_symbols.len()))]\n                .to_vec();\n            chunks.push(chunk);\n        }\n        debug!(\"{} {} {}\", real_symbols.len(), num_topics_per_connection, chunks.len(),);\n        assert!(chunks.len() > 1);\n\n        let mut last_ws_client = None;\n        let mut handles = Vec::new();\n        {\n            let n = chunks.len();\n            for (i, chunk) in chunks.into_iter().enumerate() {\n                last_ws_client = if i == (n - 1) {\n                    let tx_clone = tx.clone();\n                    Some(create_ws_client(exchange, market_type, msg_type, tx_clone).await)\n                } else {\n                    None\n                };\n                let ret = crawl_event_one_chunk(\n                    exchange.to_string(),\n                    market_type,\n                    msg_type,\n                    last_ws_client.clone(),\n                    chunk,\n                    tx.clone(),\n                );\n                handles.push(ret.await);\n            }\n            drop(tx);\n        }\n        if automatic_symbol_discovery && last_ws_client.is_some() {\n            create_new_symbol_receiver_thread(\n                exchange.to_string(),\n                msg_type,\n                market_type,\n                rx_symbols,\n                last_ws_client.unwrap(),\n            );\n        }\n        for handle in handles {\n            if let Err(err) = handle.await {\n                panic!(\"{}\", err); // TODO: use tokio::task::JoinSet or futures::stream::FuturesUnordered\n            }\n        }\n    };\n    _ = stop_ch_tx.send(EmptyStruct {});\n    if let Some(thread) = symbol_discovery_thread {\n        _ = thread.await;\n    }\n}\n\n// from 1m to 5m\nfn get_candlestick_intervals(exchange: &str, market_type: MarketType) -> Vec<usize> {\n    match exchange {\n        \"binance\" => vec![60, 180, 300],\n        \"bybit\" => vec![60, 180, 300],\n        \"deribit\" => vec![60, 180, 300],\n        \"gate\" => vec![10, 60, 300],\n        \"kucoin\" => match market_type {\n            MarketType::Spot => vec![60, 300], // Reduced to avoid Broken pipe (os error 32)\n            _ => vec![60, 300],\n        },\n        \"okx\" => vec![60, 180, 300],\n        \"zb\" => match market_type {\n            MarketType::Spot => vec![60, 180, 300],\n            MarketType::LinearSwap => vec![60, 300],\n            _ => vec![60, 180, 300],\n        },\n        \"zbg\" => match market_type {\n            MarketType::Spot => vec![60, 300],\n            _ => vec![60, 180, 300],\n        },\n        _ => vec![60, 300],\n    }\n}\n\nasync fn crawl_candlestick_one_chunk(\n    exchange: String,\n    market_type: MarketType,\n    ws_client: Option<Arc<dyn WSClient + Send + Sync>>,\n    symbol_interval_list: Vec<(String, usize)>,\n    tx: Sender<Message>,\n) -> tokio::task::JoinHandle<()> {\n    let ws_client = if let Some(ws_client) = ws_client {\n        ws_client\n    } else {\n        let tx_clone = tx.clone();\n        create_ws_client(&exchange, market_type, MessageType::Candlestick, tx_clone).await\n    };\n\n    {\n        // fire and forget\n        let ws_client_clone = ws_client.clone();\n        tokio::task::spawn(async move {\n            ws_client_clone.subscribe_candlestick(&symbol_interval_list).await;\n        });\n    }\n\n    tokio::task::spawn(async move {\n        ws_client.run().await;\n        ws_client.close().await;\n    })\n}\n\npub(crate) async fn crawl_candlestick_ext(\n    exchange: &str,\n    market_type: MarketType,\n    symbol_interval_list: Option<&[(String, usize)]>,\n    tx: Sender<Message>,\n) {\n    let num_topics_per_connection = get_num_subscriptions_per_connection(exchange, market_type);\n    let is_empty = match symbol_interval_list {\n        Some(list) => {\n            if list.is_empty() {\n                true\n            } else {\n                let symbols: Vec<String> = list.iter().map(|t| t.0.clone()).collect();\n                tokio::task::block_in_place(move || check_args(exchange, market_type, &symbols));\n                false\n            }\n        }\n        None => true,\n    };\n    let automatic_symbol_discovery = is_empty;\n\n    let symbol_interval_list: Vec<(String, usize)> = if is_empty {\n        let symbols =\n            tokio::task::block_in_place(move || fetch_symbols_retry(exchange, market_type));\n        let intervals = get_candlestick_intervals(exchange, market_type);\n        symbols\n            .iter()\n            .flat_map(|symbol| {\n                intervals.clone().into_iter().map(move |interval| (symbol.clone(), interval))\n            })\n            .collect::<Vec<(String, usize)>>()\n    } else {\n        symbol_interval_list.unwrap().to_vec()\n    };\n    if symbol_interval_list.is_empty() {\n        error!(\"symbol_interval_list is empty due to fetch_symbols_retry() failure\");\n        return;\n    }\n    let real_symbols: Vec<String> = symbol_interval_list.iter().map(|t| t.0.clone()).collect();\n    let real_intervals: Vec<usize> = symbol_interval_list.iter().map(|t| t.1).collect();\n\n    // The stop channel is used by all tokio tasks\n    let (stop_ch_tx, stop_ch_rx) = tokio::sync::broadcast::channel::<EmptyStruct>(1);\n\n    // create a thread to discover new symbols\n    let (tx_symbols, rx_symbols) = tokio::sync::mpsc::channel::<Vec<String>>(4);\n    let symbol_discovery_thread = if automatic_symbol_discovery {\n        let thread = create_symbol_discovery_thread(\n            exchange.to_string(),\n            market_type,\n            real_symbols,\n            stop_ch_rx,\n            tx_symbols,\n        );\n        Some(thread)\n    } else {\n        None\n    };\n\n    if symbol_interval_list.len() <= num_topics_per_connection {\n        let ws_client = create_ws_client(exchange, market_type, MessageType::Candlestick, tx).await;\n        ws_client.subscribe_candlestick(&symbol_interval_list).await;\n        if automatic_symbol_discovery {\n            create_new_symbol_receiver_thread_candlestick(\n                real_intervals,\n                rx_symbols,\n                ws_client.clone(),\n            );\n        }\n        ws_client.run().await;\n        ws_client.close().await;\n    } else {\n        // split to chunks\n        let mut chunks: Vec<Vec<(String, usize)>> = Vec::new();\n        {\n            for i in (0..symbol_interval_list.len()).step_by(num_topics_per_connection) {\n                let chunk: Vec<(String, usize)> = symbol_interval_list\n                    [i..(std::cmp::min(i + num_topics_per_connection, symbol_interval_list.len()))]\n                    .to_vec();\n                chunks.push(chunk);\n            }\n        }\n        debug!(\"{} {} {}\", symbol_interval_list.len(), num_topics_per_connection, chunks.len(),);\n        assert!(chunks.len() > 1);\n\n        let mut last_ws_client = None;\n        let mut handles = Vec::new();\n        {\n            let n = chunks.len();\n            for (i, chunk) in chunks.into_iter().enumerate() {\n                last_ws_client = if i == (n - 1) {\n                    let tx_clone = tx.clone();\n                    Some(\n                        create_ws_client(exchange, market_type, MessageType::Candlestick, tx_clone)\n                            .await,\n                    )\n                } else {\n                    None\n                };\n                let ret = crawl_candlestick_one_chunk(\n                    exchange.to_string(),\n                    market_type,\n                    last_ws_client.clone(),\n                    chunk,\n                    tx.clone(),\n                );\n                handles.push(ret.await);\n            }\n            drop(tx);\n        }\n        if automatic_symbol_discovery && last_ws_client.is_some() {\n            create_new_symbol_receiver_thread_candlestick(\n                real_intervals,\n                rx_symbols,\n                last_ws_client.unwrap(),\n            );\n        }\n        for handle in handles {\n            if let Err(err) = handle.await {\n                panic!(\"{}\", err); // TODO: use tokio::task::JoinSet or futures::stream::FuturesUnordered\n            }\n        }\n    };\n    _ = stop_ch_tx.send(EmptyStruct {});\n    if let Some(thread) = symbol_discovery_thread {\n        _ = thread.await;\n    }\n}\n"
  },
  {
    "path": "crypto-crawler/src/crawlers/zb.rs",
    "content": "use std::sync::mpsc::Sender;\n\nuse crate::{crawlers::utils::crawl_event, msg::Message};\nuse crypto_market_type::MarketType;\nuse crypto_msg_type::MessageType;\nuse crypto_ws_client::*;\n\nuse super::utils::create_conversion_thread;\n\nconst EXCHANGE_NAME: &str = \"zb\";\n\npub(crate) async fn crawl_ticker(\n    market_type: MarketType,\n    symbols: Option<&[String]>,\n    tx: Sender<Message>,\n) {\n    if market_type == MarketType::LinearSwap && (symbols.is_none() || symbols.unwrap().is_empty()) {\n        let tx = create_conversion_thread(\n            EXCHANGE_NAME.to_string(),\n            MessageType::Ticker,\n            market_type,\n            tx,\n        );\n        let commands: Vec<String> =\n            vec![r#\"{\"action\": \"subscribe\",\"channel\": \"All.Ticker\"}\"#.to_string()];\n\n        let ws_client = ZbSwapWSClient::new(tx, None).await;\n        ws_client.send(&commands).await;\n        ws_client.run().await;\n        ws_client.close().await;\n    } else {\n        crawl_event(EXCHANGE_NAME, MessageType::Ticker, market_type, symbols, tx).await;\n    }\n}\n"
  },
  {
    "path": "crypto-crawler/src/crawlers/zbg.rs",
    "content": "use std::sync::mpsc::Sender;\n\nuse crate::{crawlers::utils::crawl_event, msg::Message};\nuse crypto_market_type::MarketType;\nuse crypto_msg_type::MessageType;\nuse crypto_ws_client::*;\n\nuse super::utils::create_conversion_thread;\n\nconst EXCHANGE_NAME: &str = \"zbg\";\n\npub(crate) async fn crawl_ticker(\n    market_type: MarketType,\n    symbols: Option<&[String]>,\n    tx: Sender<Message>,\n) {\n    if symbols.is_none() || symbols.unwrap().is_empty() {\n        if market_type == MarketType::Spot {\n            let tx = create_conversion_thread(\n                EXCHANGE_NAME.to_string(),\n                MessageType::Ticker,\n                market_type,\n                tx,\n            );\n            let commands: Vec<String> =\n                vec![r#\"{\"action\":\"ADD\", \"dataType\":\"ALL_TRADE_STATISTIC_24H\"}\"#.to_string()];\n\n            let ws_client = ZbgSpotWSClient::new(tx, None).await;\n            ws_client.send(&commands).await;\n            ws_client.run().await;\n            ws_client.close().await;\n        } else {\n            let tx = create_conversion_thread(\n                EXCHANGE_NAME.to_string(),\n                MessageType::Ticker,\n                market_type,\n                tx,\n            );\n            let commands: Vec<String> =\n                vec![r#\"{\"action\":\"sub\", \"topic\":\"future_all_indicator\"}\"#.to_string()];\n\n            let ws_client = ZbgSwapWSClient::new(tx, None).await;\n            ws_client.send(&commands).await;\n            ws_client.run().await;\n            ws_client.close().await;\n        }\n    } else {\n        crawl_event(EXCHANGE_NAME, MessageType::Ticker, market_type, symbols, tx).await;\n    }\n}\n"
  },
  {
    "path": "crypto-crawler/src/lib.rs",
    "content": "//! A rock-solid cryprocurrency crawler.\n//!\n//! ## Crawl realtime trades\n//!\n//! ```rust\n//! use crypto_crawler::{crawl_trade, MarketType};\n//!\n//! #[tokio::main(flavor = \"multi_thread\")]\n//! async fn main() {\n//!     let (tx, rx) = std::sync::mpsc::channel();\n//!     tokio::task::spawn(async move {\n//!         // Crawl realtime trades for all symbols of binance inverse_swap markets\n//!         crawl_trade(\"binance\", MarketType::InverseSwap, None, tx).await;\n//!     });\n//!\n//!     let mut messages = Vec::new();\n//!     for msg in rx {\n//!         messages.push(msg);\n//!         break;\n//!     }\n//!     assert!(!messages.is_empty());\n//! }\n//! ```\n//!\n//! ## Crawl realtime level2 orderbook incremental updates\n//!\n//! ```rust\n//! use crypto_crawler::{crawl_l2_event, MarketType};\n//!\n//! #[tokio::main(flavor = \"multi_thread\")]\n//! async fn main() {\n//!     let (tx, rx) = std::sync::mpsc::channel();\n//!     tokio::task::spawn(async move {\n//!         // Crawl realtime level2 incremental updates for all symbols of binance inverse_swap markets\n//!         crawl_l2_event(\"binance\", MarketType::InverseSwap, None, tx).await;\n//!     });\n//!\n//!     let mut messages = Vec::new();\n//!     for msg in rx {\n//!         messages.push(msg);\n//!         break;\n//!     }\n//!     assert!(!messages.is_empty());\n//! }\n//! ```\n//!\n//! ## Crawl level2 orderbook full snapshots from RESTful API\n//!\n//! ```rust\n//! use crypto_crawler::{crawl_l2_snapshot, MarketType};\n//!\n//! let (tx, rx) = std::sync::mpsc::channel();\n//! std::thread::spawn(move || {\n//!     // Crawl level2 full snapshots for all symbols of binance inverse_swap markets\n//!     crawl_l2_snapshot(\"binance\", MarketType::InverseSwap, None, tx);\n//! });\n//!\n//! let mut messages = Vec::new();\n//! for msg in rx {\n//!     messages.push(msg);\n//!     break;\n//! }\n//! assert!(!messages.is_empty());\n//! ```\n//!\n//! ## Crawl realtime level2 orderbook top-K snapshots\n//!\n//! ```rust\n//! use crypto_crawler::{crawl_l2_topk, MarketType};\n//!\n//! #[tokio::main(flavor = \"multi_thread\")]\n//! async fn main() {\n//!     let (tx, rx) = std::sync::mpsc::channel();\n//!     tokio::task::spawn(async move {\n//!         // Crawl realtime level2 top-k snapshots for all symbols of binance inverse_swap markets\n//!         crawl_l2_topk(\"binance\", MarketType::InverseSwap, None, tx).await;\n//!     });\n//!\n//!     let mut messages = Vec::new();\n//!     for msg in rx {\n//!         messages.push(msg);\n//!         break;\n//!     }\n//!     assert!(!messages.is_empty());\n//! }\n//! ```\n//!\n//! ## Crawl realtime level3 orderbook incremental updates\n//!\n//! ```rust\n//! use crypto_crawler::{crawl_l3_event, MarketType};\n//!\n//! #[tokio::main(flavor = \"multi_thread\")]\n//! async fn main() {\n//!     let (tx, rx) = std::sync::mpsc::channel();\n//!     tokio::task::spawn(async move {\n//!         // Crawl realtime level3 updates for all symbols of CoinbasePro spot market\n//!         crawl_l3_event(\"coinbase_pro\", MarketType::Spot, None, tx).await;\n//!     });\n//!\n//!     let mut messages = Vec::new();\n//!     for msg in rx {\n//!         messages.push(msg);\n//!         break;\n//!     }\n//!     assert!(!messages.is_empty());\n//! }\n//! ```\n//!\n//! ## Crawl level3 orderbook full snapshots from RESTful API\n//!\n//! ```rust\n//! use crypto_crawler::{crawl_l3_snapshot, MarketType};\n//!\n//! let (tx, rx) = std::sync::mpsc::channel();\n//! std::thread::spawn(move || {\n//!     // Crawl level3 orderbook full snapshots for all symbols of CoinbasePro spot markets\n//!     crawl_l3_snapshot(\"coinbase_pro\", MarketType::Spot, None, tx);\n//! });\n//!\n//! let mut messages = Vec::new();\n//! for msg in rx {\n//!     messages.push(msg);\n//!     break;\n//! }\n//! assert!(!messages.is_empty());\n//! ```\n//!\n//! ## Crawl realtime BBO\n//!\n//! ```rust\n//! use crypto_crawler::{crawl_bbo, MarketType};\n//!\n//! #[tokio::main(flavor = \"multi_thread\")]\n//! async fn main() {\n//!     let (tx, rx) = std::sync::mpsc::channel();\n//!     tokio::task::spawn(async move {\n//!         // Crawl realtime best bid and ask messages for all symbols of binance COIN-margined perpetual markets\n//!         crawl_bbo(\"binance\", MarketType::InverseSwap, None, tx).await;\n//!     });\n//!\n//!     let mut messages = Vec::new();\n//!     for msg in rx {\n//!         messages.push(msg);\n//!         break;\n//!     }\n//!     assert!(!messages.is_empty());\n//! }\n//! ```\n//!\n//! ## Crawl 24hr rolling window tickers\n//!\n//! ```rust\n//! use crypto_crawler::{crawl_ticker, MarketType};\n//!\n//! #[tokio::main(flavor = \"multi_thread\")]\n//! async fn main() {\n//!     let (tx, rx) = std::sync::mpsc::channel();\n//!     tokio::task::spawn(async move {\n//!         // Crawl 24hr rolling window tickers for all symbols of binance COIN-margined perpetual markets\n//!         crawl_ticker(\"binance\", MarketType::InverseSwap, None, tx).await;\n//!     });\n//!\n//!     let mut messages = Vec::new();\n//!     for msg in rx {\n//!         messages.push(msg);\n//!         break;\n//!     }\n//!     assert!(!messages.is_empty());\n//! }\n//! ```\n//!\n//! ## Crawl candlesticks(i.e., OHLCV)\n//!\n//! ```rust\n//! use crypto_crawler::{crawl_candlestick, MarketType};\n//!\n//! #[tokio::main(flavor = \"multi_thread\")]\n//! async fn main() {\n//!     let (tx, rx) = std::sync::mpsc::channel();\n//!     tokio::task::spawn(async move {\n//!         // Crawl candlesticks from 1 minute to 3 minutes for all symbols of binance COIN-margined perpetual markets\n//!         crawl_candlestick(\"binance\", MarketType::InverseSwap, None, tx).await;\n//!     });\n//!\n//!     let mut messages = Vec::new();\n//!     for msg in rx {\n//!         messages.push(msg);\n//!         break;\n//!     }\n//!     assert!(!messages.is_empty());\n//! }\n//! ```\n//!\n//! ## Crawl funding rates\n//!\n//! ```rust\n//! use crypto_crawler::{crawl_funding_rate, MarketType};\n//!\n//! #[tokio::main(flavor = \"multi_thread\")]\n//! async fn main() {\n//!     let (tx, rx) = std::sync::mpsc::channel();\n//!     tokio::task::spawn(async move {\n//!         // Crawl funding rates for all symbols of binance COIN-margined perpetual markets\n//!         crawl_funding_rate(\"binance\", MarketType::InverseSwap, None, tx).await;\n//!     });\n//!\n//!     let mut messages = Vec::new();\n//!     for msg in rx {\n//!         messages.push(msg);\n//!         break;\n//!     }\n//!     assert!(!messages.is_empty());\n//! }\n//! ```\nmod crawlers;\nmod msg;\nmod utils;\n\nuse std::sync::mpsc::Sender;\n\npub use crawlers::fetch_symbols_retry;\npub use crypto_market_type::MarketType;\npub use crypto_msg_type::MessageType;\npub use msg::*;\npub use utils::get_hot_spot_symbols;\n\n/// Crawl realtime trades.\n///\n/// If `symbols` is None or empty, this API will crawl realtime trades for all\n/// symbols in the `market_type` market, and launch a thread to discover new\n/// symbols every hour. And so forth for all other APIs.\npub async fn crawl_trade(\n    exchange: &str,\n    market_type: MarketType,\n    symbols: Option<&[String]>,\n    tx: Sender<Message>,\n) {\n    match exchange {\n        \"binance\" => crawlers::binance::crawl_trade(market_type, symbols, tx).await,\n        \"bitmex\" => crawlers::bitmex::crawl_trade(market_type, symbols, tx).await,\n        \"deribit\" => crawlers::deribit::crawl_trade(market_type, symbols, tx).await,\n        \"bitfinex\" | \"bitget\" | \"bithumb\" | \"bitstamp\" | \"bitz\" | \"bybit\" | \"coinbase_pro\"\n        | \"dydx\" | \"ftx\" | \"gate\" | \"huobi\" | \"kraken\" | \"kucoin\" | \"mexc\" | \"okx\" | \"zb\"\n        | \"zbg\" => {\n            crawlers::crawl_event(exchange, MessageType::Trade, market_type, symbols, tx).await\n        }\n        _ => panic!(\"{exchange} does NOT have the trade websocket channel\"),\n    }\n}\n\n/// Crawl level2 orderbook update events.\npub async fn crawl_l2_event(\n    exchange: &str,\n    market_type: MarketType,\n    symbols: Option<&[String]>,\n    tx: Sender<Message>,\n) {\n    match exchange {\n        \"bitmex\" => crawlers::bitmex::crawl_l2_event(market_type, symbols, tx).await,\n        \"huobi\" => crawlers::huobi::crawl_l2_event(market_type, symbols, tx).await,\n        \"binance\" | \"bitfinex\" | \"bitget\" | \"bithumb\" | \"bitstamp\" | \"bitz\" | \"bybit\"\n        | \"coinbase_pro\" | \"deribit\" | \"dydx\" | \"ftx\" | \"gate\" | \"kraken\" | \"kucoin\" | \"mexc\"\n        | \"okx\" | \"zb\" | \"zbg\" => {\n            crawlers::crawl_event(exchange, MessageType::L2Event, market_type, symbols, tx).await\n        }\n        _ => panic!(\"{exchange} does NOT have the incremental level2 websocket channel\"),\n    }\n}\n\n/// Crawl level3 orderbook update events.\npub async fn crawl_l3_event(\n    exchange: &str,\n    market_type: MarketType,\n    symbols: Option<&[String]>,\n    tx: Sender<Message>,\n) {\n    match exchange {\n        \"bitfinex\" | \"bitstamp\" | \"coinbase_pro\" | \"kucoin\" => {\n            crawlers::crawl_event(exchange, MessageType::L3Event, market_type, symbols, tx).await\n        }\n        _ => panic!(\"{exchange} does NOT have the incremental level3 websocket channel\"),\n    }\n}\n\n/// Crawl level2 orderbook snapshots through RESTful APIs.\npub fn crawl_l2_snapshot(\n    exchange: &str,\n    market_type: MarketType,\n    symbols: Option<&[String]>,\n    tx: Sender<Message>,\n) {\n    crawlers::crawl_snapshot(exchange, market_type, MessageType::L2Snapshot, symbols, tx);\n}\n\n/// Crawl best bid and ask.\npub async fn crawl_bbo(\n    exchange: &str,\n    market_type: MarketType,\n    symbols: Option<&[String]>,\n    tx: Sender<Message>,\n) {\n    match exchange {\n        \"binance\" => crawlers::binance::crawl_bbo(market_type, symbols, tx).await,\n        \"bitmex\" => crawlers::bitmex::crawl_bbo(market_type, symbols, tx).await,\n        \"kucoin\" => crawlers::kucoin::crawl_bbo(market_type, symbols, tx).await,\n        \"deribit\" | \"ftx\" | \"gate\" | \"huobi\" | \"kraken\" | \"okx\" => {\n            crawlers::crawl_event(exchange, MessageType::BBO, market_type, symbols, tx).await\n        }\n        _ => panic!(\"{exchange} does NOT have BBO websocket channel\"),\n    }\n}\n\n/// Crawl level2 orderbook top-k snapshots through websocket.\npub async fn crawl_l2_topk(\n    exchange: &str,\n    market_type: MarketType,\n    symbols: Option<&[String]>,\n    tx: Sender<Message>,\n) {\n    match exchange {\n        \"bitmex\" => crawlers::bitmex::crawl_l2_topk(market_type, symbols, tx).await,\n        \"binance\" | \"bitget\" | \"bybit\" | \"bitstamp\" | \"deribit\" | \"gate\" | \"huobi\" | \"kucoin\"\n        | \"mexc\" | \"okx\" | \"zb\" => {\n            crawlers::crawl_event(exchange, MessageType::L2TopK, market_type, symbols, tx).await\n        }\n        _ => panic!(\"{exchange} does NOT have the level2 top-k snapshot websocket channel\"),\n    }\n}\n\n/// Crawl level3 orderbook snapshots through RESTful APIs.\npub fn crawl_l3_snapshot(\n    exchange: &str,\n    market_type: MarketType,\n    symbols: Option<&[String]>,\n    tx: Sender<Message>,\n) {\n    crawlers::crawl_snapshot(exchange, market_type, MessageType::L3Snapshot, symbols, tx)\n}\n\n/// Crawl 24hr rolling window ticker.\n///\n/// If `symbols` is None, it means all trading symbols in the `market_type`,\n/// and updates the latest symbols every hour.\npub async fn crawl_ticker(\n    exchange: &str,\n    market_type: MarketType,\n    symbols: Option<&[String]>,\n    tx: Sender<Message>,\n) {\n    match exchange {\n        \"binance\" => crawlers::binance::crawl_ticker(market_type, symbols, tx).await,\n        \"bitfinex\" | \"bitget\" | \"bithumb\" | \"bitz\" | \"bybit\" | \"coinbase_pro\" | \"deribit\"\n        | \"gate\" | \"huobi\" | \"kraken\" | \"kucoin\" | \"mexc\" | \"okx\" => {\n            crawlers::crawl_event(exchange, MessageType::Ticker, market_type, symbols, tx).await\n        }\n        \"zb\" => crawlers::zb::crawl_ticker(market_type, symbols, tx).await,\n        \"zbg\" => crawlers::zbg::crawl_ticker(market_type, symbols, tx).await,\n        _ => panic!(\"{exchange} does NOT have the ticker websocket channel\"),\n    }\n}\n\n/// Crawl perpetual swap funding rates.\npub async fn crawl_funding_rate(\n    exchange: &str,\n    market_type: MarketType,\n    symbols: Option<&[String]>,\n    tx: Sender<Message>,\n) {\n    match exchange {\n        \"binance\" => crawlers::binance::crawl_funding_rate(market_type, symbols, tx).await,\n        \"bitmex\" => crawlers::bitmex::crawl_funding_rate(market_type, symbols, tx).await,\n        \"huobi\" => crawlers::huobi::crawl_funding_rate(market_type, symbols, tx).await,\n        \"okx\" => crawlers::okx::crawl_funding_rate(market_type, symbols, tx).await,\n        _ => panic!(\"{exchange} does NOT have perpetual swap market\"),\n    }\n}\n\n/// Crawl candlestick(i.e., OHLCV) data.\n///\n/// If `symbol_interval_list` is None or empty, this API will crawl candlesticks\n/// from 10 seconds to 3 minutes(if available) for all symbols.\npub async fn crawl_candlestick(\n    exchange: &str,\n    market_type: MarketType,\n    symbol_interval_list: Option<&[(String, usize)]>,\n    tx: Sender<Message>,\n) {\n    match exchange {\n        \"bitmex\" => {\n            crawlers::bitmex::crawl_candlestick(market_type, symbol_interval_list, tx).await\n        }\n        \"binance\" | \"bitfinex\" | \"bitget\" | \"bitz\" | \"bybit\" | \"deribit\" | \"gate\" | \"huobi\"\n        | \"kraken\" | \"kucoin\" | \"mexc\" | \"okx\" | \"zb\" | \"zbg\" => {\n            crawlers::crawl_candlestick_ext(exchange, market_type, symbol_interval_list, tx).await\n        }\n        _ => panic!(\"{exchange} does NOT have the candlestick websocket channel\"),\n    };\n}\n\n/// Crawl all open interest.\npub fn crawl_open_interest(exchange: &str, market_type: MarketType, tx: Sender<Message>) {\n    crawlers::crawl_open_interest(exchange, market_type, tx);\n}\n\n/// Subscribe to multiple message types of one symbol.\n///\n/// This API is suitable for client applications such as APP, website, etc.\n///\n/// String messages in `tx` are already parsed by `crypto-msg-parser`.\npub async fn subscribe_symbol(\n    exchange: &str,\n    market_type: MarketType,\n    symbol: &str,\n    msg_types: &[MessageType],\n    tx: Sender<String>,\n) {\n    let ws_client = crawlers::create_ws_client_symbol(exchange, market_type, tx).await;\n    let symbols = vec![symbol.to_string()];\n    let commands = crypto_msg_type::get_ws_commands(exchange, msg_types, &symbols, true, None);\n    ws_client.send(&commands).await;\n    ws_client.run().await;\n    ws_client.close().await;\n}\n"
  },
  {
    "path": "crypto-crawler/src/msg.rs",
    "content": "use crypto_market_type::MarketType;\nuse crypto_msg_type::MessageType;\nuse serde::{Deserialize, Serialize};\nuse std::{\n    convert::TryInto,\n    str::FromStr,\n    time::{SystemTime, UNIX_EPOCH},\n};\n\n/// Message represents messages received by crawlers.\n#[derive(Serialize, Deserialize)]\npub struct Message {\n    /// The exchange name, unique for each exchage\n    pub exchange: String,\n    /// Market type\n    pub market_type: MarketType,\n    /// Message type\n    pub msg_type: MessageType,\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub symbol: Option<String>,\n    /// Unix timestamp in milliseconds\n    pub received_at: u64,\n    /// the original message\n    pub json: String,\n}\n\nimpl Message {\n    pub fn new(\n        exchange: String,\n        market_type: MarketType,\n        msg_type: MessageType,\n        json: String,\n    ) -> Self {\n        Message {\n            exchange,\n            market_type,\n            msg_type,\n            symbol: None,\n            received_at: SystemTime::now()\n                .duration_since(UNIX_EPOCH)\n                .unwrap()\n                .as_millis()\n                .try_into()\n                .unwrap(),\n            json: json.trim().to_string(),\n        }\n    }\n\n    pub fn new_with_symbol(\n        exchange: String,\n        market_type: MarketType,\n        msg_type: MessageType,\n        symbol: String,\n        json: String,\n    ) -> Self {\n        let mut msg = Self::new(exchange, market_type, msg_type, json);\n        msg.symbol = Some(symbol);\n        msg\n    }\n\n    /// Convert to a TSV string.\n    ///\n    /// The `exchange`, `market_type` and `msg_type` fields are not included to\n    /// save some disk space.\n    pub fn to_tsv_string(&self) -> String {\n        let symbol = if let Some(symbol) = self.symbol.clone() { symbol } else { \"\".to_string() };\n        format!(\"{}\\t{}\\t{}\", self.received_at, symbol, self.json)\n    }\n\n    /// Convert from a TSV string.\n    pub fn from_tsv_string(exchange: &str, market_type: &str, msg_type: &str, s: &str) -> Self {\n        let v: Vec<&str> = s.split('\\t').collect();\n        assert_eq!(3, v.len());\n        let market_type = MarketType::from_str(market_type).unwrap();\n        let msg_type = MessageType::from_str(msg_type).unwrap();\n\n        let symbol = if v[1].is_empty() { None } else { Some(v[1].to_string()) };\n\n        Message {\n            exchange: exchange.to_string(),\n            market_type,\n            msg_type,\n            symbol,\n            received_at: v[0].parse::<u64>().unwrap(),\n            json: v[2].to_string(),\n        }\n    }\n}\n\nimpl std::fmt::Display for Message {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        write!(f, \"{}\", serde_json::to_string(self).unwrap())\n    }\n}\n"
  },
  {
    "path": "crypto-crawler/src/utils/cmc_rank.rs",
    "content": "use reqwest::header;\nuse serde::{Deserialize, Serialize};\nuse serde_json::Value;\nuse std::collections::HashMap;\n\nuse once_cell::sync::Lazy;\n\npub(super) static CMC_RANKS: Lazy<HashMap<String, u64>> = Lazy::new(|| {\n    // offline data, in case the network is down\n    let offline: HashMap<String, u64> = vec![\n        (\"BTC\", 1),\n        (\"ETH\", 2),\n        (\"USDT\", 3),\n        (\"BNB\", 4),\n        (\"USDC\", 5),\n        (\"XRP\", 6),\n        (\"ADA\", 7),\n        (\"BUSD\", 8),\n        (\"MATIC\", 9),\n        (\"DOGE\", 10),\n        (\"SOL\", 11),\n        (\"DOT\", 12),\n        (\"SHIB\", 13),\n        (\"LTC\", 14),\n        (\"TRX\", 15),\n        (\"AVAX\", 16),\n        (\"DAI\", 17),\n        (\"UNI\", 18),\n        (\"WBTC\", 19),\n        (\"LINK\", 20),\n        (\"ATOM\", 21),\n        (\"LEO\", 22),\n        (\"OKB\", 23),\n        (\"ETC\", 24),\n        (\"TON\", 25),\n        (\"XMR\", 26),\n        (\"FIL\", 27),\n        (\"BCH\", 28),\n        (\"LDO\", 29),\n        (\"XLM\", 30),\n        (\"APT\", 31),\n        (\"CRO\", 32),\n        (\"NEAR\", 33),\n        (\"VET\", 34),\n        (\"HBAR\", 35),\n        (\"APE\", 36),\n        (\"ALGO\", 37),\n        (\"ICP\", 38),\n        (\"QNT\", 39),\n        (\"GRT\", 40),\n        (\"FTM\", 41),\n        (\"FLOW\", 42),\n        (\"EGLD\", 43),\n        (\"MANA\", 44),\n        (\"THETA\", 45),\n        (\"EOS\", 46),\n        (\"BIT\", 47),\n        (\"AAVE\", 48),\n        (\"XTZ\", 49),\n        (\"AXS\", 50),\n        (\"SAND\", 51),\n        (\"STX\", 52),\n        (\"TUSD\", 53),\n        (\"LUNC\", 54),\n        (\"RPL\", 55),\n        (\"KLAY\", 56),\n        (\"CHZ\", 57),\n        (\"USDP\", 58),\n        (\"NEO\", 59),\n        (\"HT\", 60),\n        (\"KCS\", 61),\n        (\"BSV\", 62),\n        (\"IMX\", 63),\n        (\"MINA\", 64),\n        (\"DASH\", 65),\n        (\"CAKE\", 66),\n        (\"FXS\", 67),\n        (\"MKR\", 68),\n        (\"CRV\", 69),\n        (\"ZEC\", 70),\n        (\"USDD\", 71),\n        (\"MIOTA\", 72),\n        (\"OP\", 73),\n        (\"XEC\", 74),\n        (\"BTT\", 75),\n        (\"SNX\", 76),\n        (\"GMX\", 77),\n        (\"GUSD\", 78),\n        (\"CFX\", 79),\n        (\"GT\", 80),\n        (\"TWT\", 81),\n        (\"RUNE\", 82),\n        (\"ZIL\", 83),\n        (\"PAXG\", 84),\n        (\"AGIX\", 85),\n        (\"LRC\", 86),\n        (\"ENJ\", 87),\n        (\"OSMO\", 88),\n        (\"1INCH\", 89),\n        (\"FLR\", 90),\n        (\"DYDX\", 91),\n        (\"BAT\", 92),\n        (\"SSV\", 94),\n        (\"BONE\", 95),\n        (\"CVX\", 96),\n        (\"FEI\", 97),\n        (\"ANKR\", 98),\n        (\"CSPR\", 99),\n        (\"ETHW\", 100),\n        (\"BNX\", 101),\n        (\"NEXO\", 102),\n        (\"ROSE\", 103),\n        (\"RVN\", 104),\n        (\"LUNA\", 105),\n        (\"CELO\", 106),\n        (\"HNT\", 107),\n        (\"COMP\", 108),\n        (\"TFUEL\", 109),\n        (\"XEM\", 110),\n        (\"XDC\", 111),\n        (\"KAVA\", 112),\n        (\"RNDR\", 113),\n        (\"HOT\", 114),\n        (\"WOO\", 115),\n        (\"YFI\", 116),\n        (\"FET\", 117),\n        (\"QTUM\", 118),\n        (\"MOB\", 119),\n        (\"DCR\", 120),\n        (\"MAGIC\", 121),\n        (\"T\", 122),\n        (\"AR\", 123),\n        (\"BLUR\", 124),\n        (\"AUDIO\", 125),\n        (\"KSM\", 126),\n        (\"BAL\", 127),\n        (\"ASTR\", 128),\n        (\"ENS\", 129),\n        (\"BTG\", 130),\n        (\"SUSHI\", 131),\n        (\"JASMY\", 132),\n        (\"ONE\", 133),\n        (\"GALA\", 134),\n        (\"WAVES\", 135),\n        (\"GNO\", 136),\n        (\"USTC\", 137),\n        (\"GLM\", 138),\n        (\"IOTX\", 139),\n        (\"INJ\", 140),\n        (\"JST\", 141),\n        (\"GLMR\", 142),\n        (\"XCH\", 143),\n        (\"MASK\", 144),\n        (\"BAND\", 145),\n        (\"AMP\", 146),\n        (\"KDA\", 147),\n        (\"OCEAN\", 148),\n        (\"ICX\", 149),\n        (\"OMG\", 150),\n        (\"RSR\", 151),\n        (\"ELON\", 152),\n        (\"SC\", 153),\n        (\"FLUX\", 154),\n        (\"GMT\", 155),\n        (\"ZRX\", 156),\n        (\"CHSB\", 157),\n        (\"ONT\", 158),\n        (\"BICO\", 159),\n        (\"XCN\", 160),\n        (\"IOST\", 161),\n        (\"XYM\", 162),\n        (\"HIVE\", 163),\n        (\"DAO\", 164),\n        (\"LPT\", 165),\n        (\"SKL\", 166),\n        (\"ACH\", 167),\n        (\"CKB\", 168),\n        (\"SYN\", 169),\n        (\"BORA\", 170),\n        (\"WAXP\", 171),\n        (\"SFP\", 172),\n        (\"DGB\", 173),\n        (\"STORJ\", 174),\n        (\"SXP\", 175),\n        (\"POLY\", 176),\n        (\"EVER\", 177),\n        (\"STG\", 178),\n        (\"ZEN\", 179),\n        (\"ILV\", 180),\n        (\"KEEP\", 181),\n        (\"ELF\", 182),\n        (\"RLC\", 183),\n        (\"LSK\", 184),\n        (\"UMA\", 185),\n        (\"KNC\", 186),\n        (\"METIS\", 187),\n        (\"CELR\", 188),\n        (\"MC\", 189),\n        (\"SLP\", 190),\n        (\"PUNDIX\", 191),\n        (\"BTRST\", 192),\n        (\"RIF\", 193),\n        (\"TRAC\", 194),\n        (\"PLA\", 195),\n        (\"EWT\", 196),\n        (\"MED\", 197),\n        (\"SYS\", 198),\n        (\"SCRT\", 199),\n        (\"NFT\", 200),\n        (\"HEX\", 201),\n        (\"WTRX\", 202),\n        (\"stETH\", 203),\n        (\"BTCB\", 204),\n        (\"TMG\", 205),\n        (\"WBNB\", 206),\n        (\"FRAX\", 207),\n        (\"HBTC\", 208),\n        (\"BTTOLD\", 209),\n        (\"TNC\", 210),\n        (\"WEMIX\", 211),\n        (\"BGB\", 212),\n        (\"FTT\", 213),\n        (\"XRD\", 214),\n        (\"XAUT\", 215),\n        (\"FLOKI\", 216),\n        (\"NXM\", 217),\n        (\"BabyDoge\", 218),\n        (\"USDJ\", 219),\n        (\"ASTRAFER\", 220),\n        (\"LN\", 221),\n        (\"DFI\", 222),\n        (\"BRISE\", 223),\n        (\"MV\", 224),\n        (\"LUSD\", 225),\n        (\"EDGT\", 226),\n        (\"ANY\", 227),\n        (\"ALI\", 228),\n        (\"WEVER\", 229),\n        (\"COCOS\", 230),\n        (\"TEL\", 231),\n        (\"LYXe\", 232),\n        (\"MULTI\", 233),\n        (\"KAS\", 234),\n        (\"BDX\", 235),\n        (\"CORE\", 236),\n        (\"RON\", 237),\n        (\"VVS\", 238),\n        (\"PEOPLE\", 239),\n        (\"HFT\", 240),\n        (\"EURS\", 241),\n        (\"MX\", 242),\n        (\"API3\", 243),\n        (\"AXL\", 244),\n        (\"VGX\", 245),\n        (\"GTC\", 246),\n        (\"RBN\", 247),\n        (\"CHR\", 248),\n        (\"XNO\", 249),\n        (\"DENT\", 250),\n        (\"CVC\", 251),\n        (\"LQTY\", 252),\n        (\"CEL\", 253),\n        (\"POLYX\", 254),\n        (\"HOOK\", 255),\n        (\"XTN\", 256),\n    ]\n    .into_iter()\n    .map(|x| (x.0.to_string(), x.1))\n    .collect();\n    let online = get_cmc_ranks(1024);\n\n    if online.is_empty() { offline } else { online }\n});\n\nfn http_get(url: &str) -> Result<String, reqwest::Error> {\n    let mut headers = header::HeaderMap::new();\n    headers.insert(header::CONTENT_TYPE, header::HeaderValue::from_static(\"application/json\"));\n\n    let client = reqwest::blocking::Client::builder()\n         .default_headers(headers)\n         .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\")\n         .gzip(true)\n         .build()?;\n    let response = client.get(url).send()?;\n\n    match response.error_for_status() {\n        Ok(resp) => Ok(resp.text()?),\n        Err(error) => Err(error),\n    }\n}\n\n// Returns a map of coin to cmcRank.\nfn get_cmc_ranks(limit: i64) -> HashMap<String, u64> {\n    let mut mapping: HashMap<String, u64> = HashMap::new();\n    let url = format!(\n        \"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\"\n    );\n    if let Ok(txt) = http_get(&url) {\n        if let Ok(json_obj) = serde_json::from_str::<HashMap<String, Value>>(&txt) {\n            if let Some(data) = json_obj.get(\"data\") {\n                #[derive(Serialize, Deserialize)]\n                #[allow(non_snake_case)]\n                struct Currency {\n                    id: i64,\n                    name: String,\n                    symbol: String,\n                    cmcRank: u64,\n                }\n                let arr = data[\"cryptoCurrencyList\"].as_array().unwrap();\n                for currency in arr {\n                    let currency: Currency = serde_json::from_value(currency.clone()).unwrap();\n                    mapping.insert(currency.symbol, currency.cmcRank);\n                }\n            }\n        }\n    }\n    mapping\n}\n\npub(crate) fn sort_by_cmc_rank(exchange: &str, symbols: &mut [String]) {\n    symbols.sort_by_key(|symbol| {\n        if let Some(pair) = crypto_pair::normalize_pair(symbol, exchange) {\n            let base = pair.split('/').next().unwrap();\n            *CMC_RANKS.get(base).unwrap_or(&u64::max_value())\n        } else {\n            u64::max_value()\n        }\n    });\n}\n\n#[cfg(test)]\nmod tests {\n    use crypto_market_type::MarketType;\n\n    #[test]\n    fn test_get_cmc_ranks() {\n        let mapping = super::get_cmc_ranks(256);\n        let mut v = Vec::from_iter(mapping);\n        v.sort_by(|&(_, a), &(_, b)| a.cmp(&b));\n        for (coin, rank) in v {\n            println!(\"(\\\"{coin}\\\", {rank}),\");\n        }\n    }\n\n    #[test]\n    fn test_sort_by_cmc_rank() {\n        let mut binance_linear_swap =\n            crypto_markets::fetch_symbols(\"binance\", MarketType::LinearSwap).unwrap();\n        super::sort_by_cmc_rank(\"binance\", &mut binance_linear_swap);\n        assert_eq!(\"BTCUSDT\", binance_linear_swap[0]);\n        assert_eq!(\"BTCBUSD\", binance_linear_swap[1]);\n        assert_eq!(\"ETHUSDT\", binance_linear_swap[2]);\n        assert_eq!(\"ETHBUSD\", binance_linear_swap[3]);\n    }\n}\n"
  },
  {
    "path": "crypto-crawler/src/utils/lock.rs",
    "content": "use std::{collections::HashMap, sync::Arc};\n\nuse crypto_market_type::MarketType;\nuse fslock::LockFile;\nuse once_cell::sync::Lazy;\n\nconst EXCHANGES: &[&str] = &[\n    \"binance\",\n    \"bitfinex\",\n    \"bitget\",\n    \"bithumb\",\n    \"bitmex\",\n    \"bitstamp\",\n    \"bitz\",\n    \"bybit\",\n    \"coinbase_pro\",\n    \"deribit\",\n    \"dydx\",\n    \"ftx\",\n    \"gate\",\n    \"huobi\",\n    \"kraken\",\n    \"kucoin\",\n    \"mexc\",\n    \"okx\",\n    \"zb\",\n    \"zbg\",\n];\n\nconst EXCHANGES_WS: &[&str] = &[\"bitfinex\", \"bitz\", \"kucoin\", \"okx\"];\n\n#[allow(clippy::type_complexity)]\npub(crate) static REST_LOCKS: Lazy<\n    HashMap<String, HashMap<MarketType, Arc<std::sync::Mutex<LockFile>>>>,\n> = Lazy::new(create_all_lock_files_rest);\n\n#[allow(clippy::type_complexity)]\npub(crate) static WS_LOCKS: Lazy<\n    HashMap<String, HashMap<MarketType, Arc<tokio::sync::Mutex<LockFile>>>>,\n> = Lazy::new(create_all_lock_files_ws);\n\n/// Markets with the same endpoint will have the same file name.\nfn get_lock_file_name(exchange: &str, market_type: MarketType, prefix: &str) -> String {\n    let filename = match exchange {\n        \"binance\" => match market_type {\n            MarketType::InverseSwap | MarketType::InverseFuture => {\n                \"binance_inverse.lock\".to_string()\n            }\n            MarketType::LinearSwap | MarketType::LinearFuture => \"binance_linear.lock\".to_string(),\n            MarketType::Spot => \"binance_spot.lock\".to_string(),\n            MarketType::EuropeanOption => \"binance_option.lock\".to_string(),\n            _ => panic!(\"Unknown market_type {market_type} of {exchange}\"),\n        },\n        \"bitfinex\" => \"bitfinex.lock\".to_string(),\n        \"bitget\" => match market_type {\n            MarketType::InverseFuture | MarketType::InverseSwap | MarketType::LinearSwap => {\n                \"bitget_swap.lock\".to_string()\n            }\n            MarketType::Spot => \"bitget_spot.lock\".to_string(),\n            _ => panic!(\"Unknown market_type {market_type} of {exchange}\"),\n        },\n        \"bitmex\" => \"bitmex.lock\".to_string(),\n        \"bitz\" => match market_type {\n            MarketType::InverseSwap | MarketType::LinearSwap => \"bitz_swap.lock\".to_string(),\n            MarketType::Spot => \"bitz_spot.lock\".to_string(),\n            _ => panic!(\"Unknown market_type {market_type} of {exchange}\"),\n        },\n        \"bybit\" => {\n            if prefix == \"rest\" {\n                \"bybit.lock\".to_string()\n            } else {\n                match market_type {\n                    MarketType::InverseSwap | MarketType::InverseFuture => {\n                        \"bybit_inverse.lock\".to_string()\n                    }\n                    MarketType::LinearSwap => \"bybit_linear.lock\".to_string(),\n                    _ => panic!(\"Unknown market_type {market_type} of {exchange}\"),\n                }\n            }\n        }\n        \"deribit\" => \"deribit.lock\".to_string(),\n        \"ftx\" => \"ftx.lock\".to_string(),\n        \"gate\" => match market_type {\n            MarketType::InverseSwap | MarketType::LinearSwap => \"gate_swap.lock\".to_string(),\n            MarketType::InverseFuture | MarketType::LinearFuture => \"gate_future.lock\".to_string(),\n            MarketType::Spot => \"gate_spot.lock\".to_string(),\n            _ => panic!(\"Unknown market_type {market_type} of {exchange}\"),\n        },\n        \"kucoin\" => {\n            if prefix == \"ws\" {\n                \"kucoin.lock\".to_string()\n            } else {\n                match market_type {\n                    MarketType::InverseSwap\n                    | MarketType::LinearSwap\n                    | MarketType::InverseFuture => \"kucoin_swap.lock\".to_string(),\n                    MarketType::Spot => \"kucoin_spot.lock\".to_string(),\n                    MarketType::Unknown => \"kucoin_unknown.lock\".to_string(), // for OpenInterest\n                    _ => panic!(\"Unknown market_type {market_type} of {exchange}\"),\n                }\n            }\n        }\n        \"mexc\" => match market_type {\n            MarketType::InverseSwap | MarketType::LinearSwap => \"mexc_swap.lock\".to_string(),\n            MarketType::Spot => \"mexc_spot.lock\".to_string(),\n            _ => panic!(\"Unknown market_type {market_type} of {exchange}\"),\n        },\n        \"okx\" => \"okx.lock\".to_string(),\n        \"zb\" => match market_type {\n            MarketType::LinearSwap => \"zb_swap.lock\".to_string(),\n            MarketType::Spot => \"zb_spot.lock\".to_string(),\n            _ => panic!(\"Unknown market_type {market_type} of {exchange}\"),\n        },\n        \"zbg\" => match market_type {\n            MarketType::InverseSwap | MarketType::LinearSwap => \"zbg_swap.lock\".to_string(),\n            MarketType::Spot => \"zbg_spot.lock\".to_string(),\n            _ => panic!(\"Unknown market_type {market_type} of {exchange}\"),\n        },\n        _ => format!(\"{exchange}.{market_type}.lock\"),\n    };\n    format!(\"{prefix}.{filename}\")\n}\n\nfn create_lock_file(filename: &str) -> LockFile {\n    let dir = if std::env::var(\"DATA_DIR\").is_ok() {\n        std::path::Path::new(std::env::var(\"DATA_DIR\").unwrap().as_str()).join(\"locks\")\n    } else {\n        std::env::temp_dir().join(\"locks\")\n    };\n    let _ = std::fs::create_dir_all(&dir);\n    let file_path = dir.join(filename);\n    LockFile::open(file_path.as_path())\n        .unwrap_or_else(|_| panic!(\"{}\", file_path.to_str().unwrap().to_string()))\n}\n\nfn create_all_lock_files_rest()\n-> HashMap<String, HashMap<MarketType, Arc<std::sync::Mutex<LockFile>>>> {\n    let prefix = \"rest\";\n    // filename -> lock\n    let mut cache: HashMap<String, Arc<std::sync::Mutex<LockFile>>> = HashMap::new();\n    let mut result: HashMap<String, HashMap<MarketType, Arc<std::sync::Mutex<LockFile>>>> =\n        HashMap::new();\n    for exchange in EXCHANGES.iter() {\n        let m = result.entry(exchange.to_string()).or_insert_with(HashMap::new);\n        let mut market_types = crypto_market_type::get_market_types(exchange);\n        if *exchange == \"bitmex\" {\n            market_types.push(MarketType::Unknown);\n        }\n        if *exchange == \"deribit\" {\n            market_types.push(MarketType::Unknown);\n        }\n        if prefix == \"rest\" && (*exchange == \"ftx\" || *exchange == \"kucoin\") {\n            market_types.push(MarketType::Unknown); // for OpenInterest\n        }\n        for market_type in market_types {\n            let filename = get_lock_file_name(exchange, market_type, prefix);\n            let lock_file = cache\n                .entry(filename.clone())\n                .or_insert_with(|| Arc::new(std::sync::Mutex::new(create_lock_file(&filename))));\n            m.insert(market_type, lock_file.clone());\n        }\n    }\n    result\n}\n\nfn create_all_lock_files_ws()\n-> HashMap<String, HashMap<MarketType, Arc<tokio::sync::Mutex<LockFile>>>> {\n    let prefix = \"ws\";\n    // filename -> lock\n    let mut cache: HashMap<String, Arc<tokio::sync::Mutex<LockFile>>> = HashMap::new();\n    let mut result: HashMap<String, HashMap<MarketType, Arc<tokio::sync::Mutex<LockFile>>>> =\n        HashMap::new();\n    for exchange in EXCHANGES_WS.iter() {\n        let m = result.entry(exchange.to_string()).or_insert_with(HashMap::new);\n        let mut market_types = crypto_market_type::get_market_types(exchange);\n        if *exchange == \"bitmex\" {\n            market_types.push(MarketType::Unknown);\n        }\n        if prefix == \"rest\" && (*exchange == \"ftx\" || *exchange == \"kucoin\") {\n            market_types.push(MarketType::Unknown); // for OpenInterest\n        }\n        for market_type in market_types {\n            let filename = get_lock_file_name(exchange, market_type, prefix);\n            let lock_file = cache\n                .entry(filename.clone())\n                .or_insert_with(|| Arc::new(tokio::sync::Mutex::new(create_lock_file(&filename))));\n            m.insert(market_type, lock_file.clone());\n        }\n    }\n    result\n}\n"
  },
  {
    "path": "crypto-crawler/src/utils/mod.rs",
    "content": "pub(crate) mod cmc_rank;\nmod lock;\npub(crate) mod spot_symbols;\n\npub(crate) use lock::{REST_LOCKS, WS_LOCKS};\npub use spot_symbols::get_hot_spot_symbols;\n"
  },
  {
    "path": "crypto-crawler/src/utils/spot_symbols.rs",
    "content": "use std::collections::HashSet;\n\nuse crypto_market_type::MarketType;\n\npub fn get_hot_spot_symbols(exchange: &str, spot_symbols: &[String]) -> Vec<String> {\n    let market_types = crypto_market_type::get_market_types(exchange);\n    let cmc_ranks = &super::cmc_rank::CMC_RANKS;\n    let contract_base_coins = {\n        let mut contract_base_coins = HashSet::<String>::new();\n        for market_type in market_types.iter().filter(|m| *m != &MarketType::Spot) {\n            let symbols = crypto_markets::fetch_symbols(exchange, *market_type).unwrap_or_default();\n            for symbol in symbols {\n                let pair = crypto_pair::normalize_pair(&symbol, exchange).unwrap();\n                let base_coin = pair.split('/').next().unwrap();\n                contract_base_coins.insert(base_coin.to_string());\n            }\n        }\n        contract_base_coins\n    };\n    let is_hot = |symbol: &str| {\n        let pair = crypto_pair::normalize_pair(symbol, exchange).unwrap();\n        let base_coin = pair.split('/').next().unwrap();\n        contract_base_coins.contains(base_coin)\n            || *cmc_ranks.get(base_coin).unwrap_or(&u64::max_value()) <= 100\n    };\n\n    spot_symbols.iter().cloned().filter(|symbol| is_hot(symbol)).collect()\n}\n\n#[cfg(test)]\nmod tests {\n    use crypto_market_type::MarketType;\n\n    use super::get_hot_spot_symbols;\n\n    #[test]\n    fn test_binance() {\n        let spot_symbols = crypto_markets::fetch_symbols(\"binance\", MarketType::Spot).unwrap();\n        let symbols = get_hot_spot_symbols(\"binance\", &spot_symbols);\n        assert!(!symbols.is_empty());\n    }\n\n    #[test]\n    fn test_huobi() {\n        let spot_symbols = crypto_markets::fetch_symbols(\"huobi\", MarketType::Spot).unwrap();\n        let symbols = get_hot_spot_symbols(\"huobi\", &spot_symbols);\n        assert!(!symbols.is_empty());\n    }\n\n    #[test]\n    fn test_okx() {\n        let spot_symbols = crypto_markets::fetch_symbols(\"okx\", MarketType::Spot).unwrap();\n        let symbols = get_hot_spot_symbols(\"okx\", &spot_symbols);\n        assert!(!symbols.is_empty());\n    }\n}\n"
  },
  {
    "path": "crypto-crawler/tests/binance.rs",
    "content": "#[macro_use]\nmod utils;\n\nuse test_case::test_case;\n\nuse crypto_crawler::*;\nuse crypto_market_type::MarketType;\nuse crypto_msg_type::MessageType;\n\nuse utils::parse;\n\nconst EXCHANGE_NAME: &str = \"binance\";\n\n#[test_case(MarketType::Spot)]\n#[test_case(MarketType::InverseFuture)]\n#[test_case(MarketType::LinearFuture)]\n#[test_case(MarketType::InverseSwap)]\n#[test_case(MarketType::LinearSwap)]\n#[tokio::test(flavor = \"multi_thread\", worker_threads = 8)]\nasync fn test_crawl_trade_all(market_type: MarketType) {\n    test_all_symbols!(crawl_trade, EXCHANGE_NAME, market_type, MessageType::Trade)\n}\n\n#[test_case(MarketType::Spot, \"BTCUSDT\")]\n#[test_case(MarketType::InverseFuture, \"BTCUSD_221230\")]\n#[test_case(MarketType::LinearFuture, \"BTCUSDT_221230\")]\n#[test_case(MarketType::InverseSwap, \"BTCUSD_PERP\")]\n#[test_case(MarketType::LinearSwap, \"BTCUSDT\")]\n// #[test_case(MarketType::EuropeanOption, \"BTC-220610-30000-C\"; \"ignore\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_trade(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_trade, EXCHANGE_NAME, market_type, symbol, MessageType::Trade)\n}\n\n#[test_case(MarketType::Spot, \"BTCUSDT\")]\n#[test_case(MarketType::InverseFuture, \"BTCUSD_221230\")]\n#[test_case(MarketType::LinearFuture, \"BTCUSDT_221230\")]\n#[test_case(MarketType::InverseSwap, \"BTCUSD_PERP\")]\n#[test_case(MarketType::LinearSwap, \"BTCUSDT\")]\n// #[test_case(MarketType::EuropeanOption, \"BTC-220610-30000-C\"; \"ignore\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_l2_event(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_l2_event, EXCHANGE_NAME, market_type, symbol, MessageType::L2Event)\n}\n\n#[test_case(MarketType::Spot, \"BTCUSDT\")]\n#[test_case(MarketType::InverseFuture, \"BTCUSD_221230\")]\n#[test_case(MarketType::LinearFuture, \"BTCUSDT_221230\")]\n#[test_case(MarketType::InverseSwap, \"BTCUSD_PERP\")]\n#[test_case(MarketType::LinearSwap, \"BTCUSDT\")]\n// #[test_case(MarketType::EuropeanOption, \"BTC-220610-30000-C\"; \"ignore\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_bbo(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_bbo, EXCHANGE_NAME, market_type, symbol, MessageType::BBO)\n}\n\n#[test_case(MarketType::Spot, \"BTCUSDT\")]\n#[test_case(MarketType::InverseFuture, \"BTCUSD_221230\")]\n#[test_case(MarketType::LinearFuture, \"BTCUSDT_221230\")]\n#[test_case(MarketType::InverseSwap, \"BTCUSD_PERP\")]\n#[test_case(MarketType::LinearSwap, \"BTCUSDT\")]\n// #[test_case(MarketType::EuropeanOption, \"BTC-220610-30000-C\"; \"ignore\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_l2_topk(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_l2_topk, EXCHANGE_NAME, market_type, symbol, MessageType::L2TopK)\n}\n\n#[test_case(MarketType::Spot, \"BTCUSDT\")]\n#[test_case(MarketType::InverseFuture, \"BTCUSD_221230\")]\n#[test_case(MarketType::LinearFuture, \"BTCUSDT_221230\")]\n#[test_case(MarketType::InverseSwap, \"BTCUSD_PERP\")]\n#[test_case(MarketType::LinearSwap, \"BTCUSDT\")]\n// #[test_case(MarketType::EuropeanOption, \"BTC-220610-30000-C\")]\nfn test_crawl_l2_snapshot(market_type: MarketType, symbol: &str) {\n    test_crawl_restful!(\n        crawl_l2_snapshot,\n        EXCHANGE_NAME,\n        market_type,\n        symbol,\n        MessageType::L2Snapshot\n    )\n}\n\n#[test_case(MarketType::Spot)]\n#[test_case(MarketType::InverseFuture)]\n#[test_case(MarketType::LinearFuture)]\n#[test_case(MarketType::InverseSwap)]\n#[test_case(MarketType::LinearSwap)]\n// #[test_case(MarketType::EuropeanOption)]\nfn test_crawl_l2_snapshot_without_symbol(market_type: MarketType) {\n    test_crawl_restful_all_symbols!(\n        crawl_l2_snapshot,\n        EXCHANGE_NAME,\n        market_type,\n        MessageType::L2Snapshot\n    )\n}\n\n#[test_case(MarketType::Spot, \"BTCUSDT\")]\n#[test_case(MarketType::InverseFuture, \"BTCUSD_221230\")]\n#[test_case(MarketType::LinearFuture, \"BTCUSDT_221230\")]\n#[test_case(MarketType::InverseSwap, \"BTCUSD_PERP\")]\n#[test_case(MarketType::LinearSwap, \"BTCUSDT\")]\n// #[test_case(MarketType::EuropeanOption, \"BTC-220610-30000-C\"; \"ignore\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_ticker(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_ticker, EXCHANGE_NAME, market_type, symbol, MessageType::Ticker)\n}\n\n#[test_case(MarketType::InverseSwap, \"BTCUSD_PERP\")]\n#[test_case(MarketType::LinearSwap, \"BTCUSDT\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_funding_rate(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(\n        crawl_funding_rate,\n        EXCHANGE_NAME,\n        market_type,\n        symbol,\n        MessageType::FundingRate\n    )\n}\n\n#[test_case(MarketType::Spot)]\n#[test_case(MarketType::InverseFuture)]\n#[test_case(MarketType::LinearFuture)]\n#[test_case(MarketType::InverseSwap)]\n#[test_case(MarketType::LinearSwap)]\n#[tokio::test(flavor = \"multi_thread\", worker_threads = 8)]\nasync fn test_crawl_candlestick(market_type: MarketType) {\n    gen_test_crawl_candlestick!(EXCHANGE_NAME, market_type)\n}\n\n// #[test_case(MarketType::Spot, \"BTCUSDT\")]\n// #[test_case(MarketType::InverseFuture, \"BTCUSD_221230\")]\n// #[test_case(MarketType::LinearFuture, \"BTCUSDT_221230\")]\n// #[test_case(MarketType::InverseSwap, \"BTCUSD_PERP\")]\n// #[test_case(MarketType::LinearSwap, \"BTCUSDT\")]\n// #[test_case(MarketType::EuropeanOption, \"BTC-220610-30000-C\"; \"ignore\")]\n// #[tokio::test(flavor = \"multi_thread\")]\n// async fn test_subscribe_symbol(market_type: MarketType, symbol: &str) {\n//     gen_test_subscribe_symbol!(EXCHANGE_NAME, market_type, symbol)\n// }\n"
  },
  {
    "path": "crypto-crawler/tests/bitfinex.rs",
    "content": "#[macro_use]\nmod utils;\n\nuse test_case::test_case;\n\nuse crypto_crawler::*;\nuse crypto_market_type::MarketType;\nuse crypto_msg_type::MessageType;\nuse utils::parse;\n\nconst EXCHANGE_NAME: &str = \"bitfinex\";\n\n#[test_case(MarketType::Spot)]\n#[test_case(MarketType::LinearSwap)]\n#[tokio::test(flavor = \"multi_thread\", worker_threads = 8)]\nasync fn test_crawl_trade_all(market_type: MarketType) {\n    test_all_symbols!(crawl_trade, EXCHANGE_NAME, market_type, MessageType::Trade)\n}\n\n#[test_case(MarketType::Spot, \"tBTCUSD\")]\n#[test_case(MarketType::LinearSwap, \"tBTCF0:USTF0\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_trade(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_trade, EXCHANGE_NAME, market_type, symbol, MessageType::Trade)\n}\n\n#[test_case(MarketType::Spot, \"tBTCUSD\")]\n#[test_case(MarketType::LinearSwap, \"tBTCF0:USTF0\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_l2_event(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_l2_event, EXCHANGE_NAME, market_type, symbol, MessageType::L2Event)\n}\n\n#[test_case(MarketType::Spot, \"tBTCUSD\")]\n#[test_case(MarketType::LinearSwap, \"tBTCF0:USTF0\")]\nfn test_crawl_l2_snapshot(market_type: MarketType, symbol: &str) {\n    test_crawl_restful!(\n        crawl_l2_snapshot,\n        EXCHANGE_NAME,\n        market_type,\n        symbol,\n        MessageType::L2Snapshot\n    )\n}\n\n#[test_case(MarketType::Spot)]\n#[test_case(MarketType::LinearSwap)]\nfn test_crawl_l2_snapshot_without_symbol(market_type: MarketType) {\n    test_crawl_restful_all_symbols!(\n        crawl_l2_snapshot,\n        EXCHANGE_NAME,\n        market_type,\n        MessageType::L2Snapshot\n    )\n}\n\n#[test_case(MarketType::Spot, \"tBTCUSD\")]\n#[test_case(MarketType::LinearSwap, \"tBTCF0:USTF0\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_l3_event(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_l3_event, EXCHANGE_NAME, market_type, symbol, MessageType::L3Event)\n}\n\n#[test_case(MarketType::Spot, \"tBTCUSD\")]\n#[test_case(MarketType::LinearSwap, \"tBTCF0:USTF0\")]\nfn test_crawl_l3_snapshot(market_type: MarketType, symbol: &str) {\n    test_crawl_restful!(\n        crawl_l3_snapshot,\n        EXCHANGE_NAME,\n        market_type,\n        symbol,\n        MessageType::L3Snapshot\n    )\n}\n\n#[test_case(MarketType::Spot, \"tBTCUSD\")]\n#[test_case(MarketType::LinearSwap, \"tBTCF0:USTF0\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_ticker(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_ticker, EXCHANGE_NAME, market_type, symbol, MessageType::Ticker)\n}\n\n#[test_case(MarketType::Spot)]\n#[test_case(MarketType::LinearSwap)]\n#[tokio::test(flavor = \"multi_thread\", worker_threads = 8)]\nasync fn test_crawl_candlestick(market_type: MarketType) {\n    gen_test_crawl_candlestick!(EXCHANGE_NAME, market_type)\n}\n"
  },
  {
    "path": "crypto-crawler/tests/bitget.rs",
    "content": "#[macro_use]\nmod utils;\n\nuse test_case::test_case;\n\nuse crypto_crawler::*;\nuse crypto_market_type::MarketType;\nuse crypto_msg_type::MessageType;\nuse utils::parse;\n\nconst EXCHANGE_NAME: &str = \"bitget\";\n\n// #[test_case(MarketType::InverseFuture)]\n#[test_case(MarketType::InverseSwap)]\n#[test_case(MarketType::LinearSwap)]\n#[tokio::test(flavor = \"multi_thread\", worker_threads = 8)]\nasync fn test_crawl_trade_all(market_type: MarketType) {\n    test_all_symbols!(crawl_trade, EXCHANGE_NAME, market_type, MessageType::Trade)\n}\n\n// #[test_case(MarketType::InverseFuture, \"BTCUSD_DMCBL_221230\")]\n#[test_case(MarketType::InverseSwap, \"BTCUSD_DMCBL\")]\n#[test_case(MarketType::LinearSwap, \"BTCUSDT_UMCBL\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_trade(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_trade, EXCHANGE_NAME, market_type, symbol, MessageType::Trade)\n}\n\n// #[test_case(MarketType::InverseFuture, \"BTCUSD_DMCBL_221230\")]\n#[test_case(MarketType::InverseSwap, \"BTCUSD_DMCBL\")]\n#[test_case(MarketType::LinearSwap, \"BTCUSDT_UMCBL\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_l2_event(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_l2_event, EXCHANGE_NAME, market_type, symbol, MessageType::L2Event)\n}\n\n// #[test_case(MarketType::InverseFuture, \"BTCUSD_DMCBL_221230\")]\n#[test_case(MarketType::InverseSwap, \"BTCUSD_DMCBL\")]\n#[test_case(MarketType::LinearSwap, \"BTCUSDT_UMCBL\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_l2_topk(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_l2_topk, EXCHANGE_NAME, market_type, symbol, MessageType::L2TopK)\n}\n\n#[test_case(MarketType::Spot, \"BTCUSDT_SPBL\")]\n#[test_case(MarketType::InverseFuture, \"BTCUSD_DMCBL_221230\")]\n#[test_case(MarketType::InverseSwap, \"BTCUSD_DMCBL\")]\n#[test_case(MarketType::LinearSwap, \"BTCUSDT_UMCBL\")]\nfn test_crawl_l2_snapshot(market_type: MarketType, symbol: &str) {\n    test_crawl_restful!(\n        crawl_l2_snapshot,\n        EXCHANGE_NAME,\n        market_type,\n        symbol,\n        MessageType::L2Snapshot\n    )\n}\n\n#[test_case(MarketType::Spot)]\n#[test_case(MarketType::InverseFuture)]\n#[test_case(MarketType::InverseSwap)]\n#[test_case(MarketType::LinearSwap)]\nfn test_crawl_l2_snapshot_without_symbol(market_type: MarketType) {\n    test_crawl_restful_all_symbols!(\n        crawl_l2_snapshot,\n        EXCHANGE_NAME,\n        market_type,\n        MessageType::L2Snapshot\n    )\n}\n\n// #[test_case(MarketType::InverseFuture, \"BTCUSD_DMCBL_221230\")]\n#[test_case(MarketType::InverseSwap, \"BTCUSD_DMCBL\")]\n#[test_case(MarketType::LinearSwap, \"BTCUSDT_UMCBL\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_ticker(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_ticker, EXCHANGE_NAME, market_type, symbol, MessageType::Ticker)\n}\n\n// #[test_case(MarketType::InverseFuture)]\n#[test_case(MarketType::InverseSwap)]\n#[test_case(MarketType::LinearSwap)]\n#[tokio::test(flavor = \"multi_thread\", worker_threads = 16)]\nasync fn test_crawl_candlestick(market_type: MarketType) {\n    gen_test_crawl_candlestick!(EXCHANGE_NAME, market_type)\n}\n"
  },
  {
    "path": "crypto-crawler/tests/bithumb.rs",
    "content": "#[macro_use]\nmod utils;\n\nuse test_case::test_case;\n\nuse crypto_crawler::*;\nuse crypto_market_type::MarketType;\nuse crypto_msg_type::MessageType;\nuse utils::parse;\n\nconst EXCHANGE_NAME: &str = \"bithumb\";\n\n#[test_case(MarketType::Spot)]\n#[tokio::test(flavor = \"multi_thread\", worker_threads = 8)]\nasync fn test_crawl_trade_all(market_type: MarketType) {\n    test_all_symbols!(crawl_trade, EXCHANGE_NAME, market_type, MessageType::Trade)\n}\n\n#[test_case(MarketType::Spot, \"BTC-USDT\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_trade(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_trade, EXCHANGE_NAME, market_type, symbol, MessageType::Trade)\n}\n\n#[test_case(MarketType::Spot, \"BTC-USDT\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_l2_event(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_l2_event, EXCHANGE_NAME, market_type, symbol, MessageType::L2Event)\n}\n\n#[test_case(MarketType::Spot, \"BTC-USDT\")]\nfn test_crawl_l2_snapshot(market_type: MarketType, symbol: &str) {\n    test_crawl_restful!(\n        crawl_l2_snapshot,\n        EXCHANGE_NAME,\n        market_type,\n        symbol,\n        MessageType::L2Snapshot\n    )\n}\n\n#[test_case(MarketType::Spot)]\nfn test_crawl_l2_snapshot_without_symbol(market_type: MarketType) {\n    test_crawl_restful_all_symbols!(\n        crawl_l2_snapshot,\n        EXCHANGE_NAME,\n        market_type,\n        MessageType::L2Snapshot\n    )\n}\n\n#[test_case(MarketType::Spot, \"BTC-USDT\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_ticker(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_ticker, EXCHANGE_NAME, market_type, symbol, MessageType::Ticker)\n}\n"
  },
  {
    "path": "crypto-crawler/tests/bitmex.rs",
    "content": "#[macro_use]\nmod utils;\n\nuse test_case::test_case;\n\nuse crypto_crawler::*;\nuse crypto_market_type::MarketType;\nuse crypto_msg_type::MessageType;\nuse utils::parse;\n\nconst EXCHANGE_NAME: &str = \"bitmex\";\n\nasync fn crawl_all(msg_type: MessageType) {\n    let (tx, rx) = std::sync::mpsc::channel();\n    tokio::task::spawn(async move {\n        match msg_type {\n            MessageType::Trade => {\n                crawl_trade(EXCHANGE_NAME, MarketType::Unknown, None, tx).await;\n            }\n            MessageType::L2Event => {\n                crawl_l2_event(EXCHANGE_NAME, MarketType::Unknown, None, tx).await;\n            }\n            MessageType::L2Snapshot => {\n                tokio::task::block_in_place(move || {\n                    crawl_l2_snapshot(EXCHANGE_NAME, MarketType::Unknown, None, tx);\n                });\n            }\n            MessageType::BBO => {\n                crawl_bbo(EXCHANGE_NAME, MarketType::Unknown, None, tx).await;\n            }\n            MessageType::L2TopK => {\n                crawl_l2_topk(EXCHANGE_NAME, MarketType::Unknown, None, tx).await;\n            }\n            MessageType::FundingRate => {\n                crawl_funding_rate(EXCHANGE_NAME, MarketType::Unknown, None, tx).await;\n            }\n            _ => panic!(\"unsupported message type {msg_type}\"),\n        };\n    });\n\n    let msg = rx.recv().unwrap();\n\n    assert_eq!(msg.exchange, EXCHANGE_NAME.to_string());\n    assert_eq!(msg.market_type, MarketType::Unknown);\n    assert_eq!(msg.msg_type, msg_type);\n}\n\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_trade_all() {\n    crawl_all(MessageType::Trade).await;\n}\n\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_l2_event_all() {\n    crawl_all(MessageType::L2Event).await;\n}\n\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_bbo_all() {\n    crawl_all(MessageType::BBO).await;\n}\n\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_l2_topk_all() {\n    crawl_all(MessageType::L2TopK).await;\n}\n\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_l2_snapshot_all() {\n    crawl_all(MessageType::L2Snapshot).await;\n}\n\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_funding_rate_all() {\n    crawl_all(MessageType::FundingRate).await;\n}\n\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_candlestick_rate_all() {\n    let (tx, rx) = std::sync::mpsc::channel();\n    tokio::task::spawn(async move {\n        crawl_candlestick(EXCHANGE_NAME, MarketType::Unknown, None, tx).await;\n    });\n\n    let msg = rx.recv().unwrap();\n\n    assert_eq!(msg.exchange, EXCHANGE_NAME.to_string());\n    assert_eq!(msg.market_type, MarketType::Unknown);\n    assert_eq!(msg.msg_type, MessageType::Candlestick);\n}\n\n#[test_case(MarketType::InverseSwap, \"XBTUSD\")]\n#[test_case(MarketType::QuantoSwap, \"ETHUSD\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_l2_event(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_l2_event, EXCHANGE_NAME, market_type, symbol, MessageType::L2Event)\n}\n\n// #[test_case(MarketType::InverseSwap, \"XBTUSD\")]\n// fn test_subscribe_symbol(market_type: MarketType, symbol: &str) {\n//     gen_test_subscribe_symbol!(EXCHANGE_NAME, market_type, symbol)\n// }\n"
  },
  {
    "path": "crypto-crawler/tests/bitstamp.rs",
    "content": "#[macro_use]\nmod utils;\n\nuse test_case::test_case;\n\nuse crypto_crawler::*;\nuse crypto_market_type::MarketType;\nuse crypto_msg_type::MessageType;\nuse utils::parse;\n\nconst EXCHANGE_NAME: &str = \"bitstamp\";\n\n#[test_case(MarketType::Spot)]\n#[tokio::test(flavor = \"multi_thread\", worker_threads = 8)]\nasync fn test_crawl_trade_all(market_type: MarketType) {\n    test_all_symbols!(crawl_trade, EXCHANGE_NAME, market_type, MessageType::Trade)\n}\n\n#[test_case(MarketType::Spot, \"btcusd\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_trade(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_trade, EXCHANGE_NAME, market_type, symbol, MessageType::Trade)\n}\n\n#[test_case(MarketType::Spot, \"btcusd\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_l2_event(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_l2_event, EXCHANGE_NAME, market_type, symbol, MessageType::L2Event)\n}\n\n#[test_case(MarketType::Spot, \"btcusd\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_l2_topk(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_l2_topk, EXCHANGE_NAME, market_type, symbol, MessageType::L2TopK)\n}\n\n#[test_case(MarketType::Spot, \"btcusd\")]\nfn test_crawl_l2_snapshot(market_type: MarketType, symbol: &str) {\n    test_crawl_restful!(\n        crawl_l2_snapshot,\n        EXCHANGE_NAME,\n        market_type,\n        symbol,\n        MessageType::L2Snapshot\n    )\n}\n\n#[test_case(MarketType::Spot)]\nfn test_crawl_l2_snapshot_without_symbol(market_type: MarketType) {\n    test_crawl_restful_all_symbols!(\n        crawl_l2_snapshot,\n        EXCHANGE_NAME,\n        market_type,\n        MessageType::L2Snapshot\n    )\n}\n\n#[test_case(MarketType::Spot, \"btcusd\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_l3_event(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_l3_event, EXCHANGE_NAME, market_type, symbol, MessageType::L3Event)\n}\n\n#[test_case(MarketType::Spot, \"btcusd\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_l3_snapshot(market_type: MarketType, symbol: &str) {\n    test_crawl_restful!(\n        crawl_l3_snapshot,\n        EXCHANGE_NAME,\n        market_type,\n        symbol,\n        MessageType::L3Snapshot\n    )\n}\n"
  },
  {
    "path": "crypto-crawler/tests/bitz.rs",
    "content": "#[macro_use]\nmod utils;\n\nuse test_case::test_case;\n\nuse crypto_crawler::*;\nuse crypto_market_type::MarketType;\nuse crypto_msg_type::MessageType;\nuse utils::parse;\n\nconst EXCHANGE_NAME: &str = \"bitz\";\n\n#[test_case(MarketType::Spot, \"btc_usdt\"; \"inconclusive\")]\n// #[test_case(MarketType::InverseSwap, \"BTC_USD\")]\n// #[test_case(MarketType::LinearSwap, \"BTC_USDT\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_trade(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_trade, EXCHANGE_NAME, market_type, symbol, MessageType::Trade)\n}\n\n#[test_case(MarketType::Spot, \"btc_usdt\"; \"inconclusive\")]\n// #[test_case(MarketType::InverseSwap, \"BTC_USD\")]\n// #[test_case(MarketType::LinearSwap, \"BTC_USDT\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_l2_event(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_l2_event, EXCHANGE_NAME, market_type, symbol, MessageType::L2Event)\n}\n\n#[test_case(MarketType::Spot, \"btc_usdt\"; \"inconclusive spot\")]\n#[test_case(MarketType::InverseSwap, \"BTC_USD\"; \"inconclusive inverse_swap\")]\n#[test_case(MarketType::LinearSwap, \"BTC_USDT\"; \"inconclusive linear_swap\")]\nfn test_crawl_l2_snapshot(market_type: MarketType, symbol: &str) {\n    test_crawl_restful!(\n        crawl_l2_snapshot,\n        EXCHANGE_NAME,\n        market_type,\n        symbol,\n        MessageType::L2Snapshot\n    )\n}\n\n#[test_case(MarketType::Spot; \"inconclusive spot\")]\n#[test_case(MarketType::InverseSwap; \"inconclusive inverse_swap\")]\n#[test_case(MarketType::LinearSwap; \"inconclusive linear_swap\")]\nfn test_crawl_l2_snapshot_without_symbol(market_type: MarketType) {\n    test_crawl_restful_all_symbols!(\n        crawl_l2_snapshot,\n        EXCHANGE_NAME,\n        market_type,\n        MessageType::L2Snapshot\n    )\n}\n\n#[test_case(MarketType::Spot, \"btc_usdt\"; \"inconclusive\")]\n// #[test_case(MarketType::InverseSwap, \"BTC_USD\")]\n// #[test_case(MarketType::LinearSwap, \"BTC_USDT\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_ticker(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_ticker, EXCHANGE_NAME, market_type, symbol, MessageType::Ticker)\n}\n\n#[test_case(MarketType::Spot; \"inconclusive\")]\n#[tokio::test(flavor = \"multi_thread\", worker_threads = 8)]\nasync fn test_crawl_candlestick(market_type: MarketType) {\n    gen_test_crawl_candlestick!(EXCHANGE_NAME, market_type)\n}\n"
  },
  {
    "path": "crypto-crawler/tests/bybit.rs",
    "content": "#[macro_use]\nmod utils;\n\nuse test_case::test_case;\n\nuse crypto_crawler::*;\nuse crypto_market_type::MarketType;\nuse crypto_msg_type::MessageType;\nuse utils::parse;\n\nconst EXCHANGE_NAME: &str = \"bybit\";\n\n#[test_case(MarketType::InverseFuture)]\n#[test_case(MarketType::InverseSwap)]\n#[test_case(MarketType::LinearSwap)]\n#[tokio::test(flavor = \"multi_thread\", worker_threads = 8)]\nasync fn test_crawl_trade_all(market_type: MarketType) {\n    test_all_symbols!(crawl_trade, EXCHANGE_NAME, market_type, MessageType::Trade)\n}\n\n#[test_case(MarketType::InverseFuture, \"BTCUSDZ22\")]\n#[test_case(MarketType::InverseSwap, \"BTCUSD\")]\n#[test_case(MarketType::LinearSwap, \"BTCUSDT\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_trade(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_trade, EXCHANGE_NAME, market_type, symbol, MessageType::Trade)\n}\n\n#[test_case(MarketType::InverseFuture, \"BTCUSDZ22\")]\n#[test_case(MarketType::InverseSwap, \"BTCUSD\")]\n#[test_case(MarketType::LinearSwap, \"BTCUSDT\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_l2_event(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_l2_event, EXCHANGE_NAME, market_type, symbol, MessageType::L2Event)\n}\n\n#[test_case(MarketType::InverseFuture, \"BTCUSDZ22\")]\n#[test_case(MarketType::InverseSwap, \"BTCUSD\")]\n#[test_case(MarketType::LinearSwap, \"BTCUSDT\")]\nfn test_crawl_l2_snapshot(market_type: MarketType, symbol: &str) {\n    test_crawl_restful!(\n        crawl_l2_snapshot,\n        EXCHANGE_NAME,\n        market_type,\n        symbol,\n        MessageType::L2Snapshot\n    )\n}\n\n#[test_case(MarketType::InverseFuture)]\n#[test_case(MarketType::InverseSwap)]\n#[test_case(MarketType::LinearSwap)]\nfn test_crawl_l2_snapshot_without_symbol(market_type: MarketType) {\n    test_crawl_restful_all_symbols!(\n        crawl_l2_snapshot,\n        EXCHANGE_NAME,\n        market_type,\n        MessageType::L2Snapshot\n    )\n}\n\n#[test_case(MarketType::InverseFuture, \"BTCUSDZ22\")]\n#[test_case(MarketType::InverseSwap, \"BTCUSD\")]\n#[test_case(MarketType::LinearSwap, \"BTCUSDT\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_ticker(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_ticker, EXCHANGE_NAME, market_type, symbol, MessageType::Ticker)\n}\n\n#[test_case(MarketType::InverseFuture)]\n#[test_case(MarketType::InverseSwap)]\n#[test_case(MarketType::LinearSwap)]\n#[tokio::test(flavor = \"multi_thread\", worker_threads = 8)]\nasync fn test_crawl_candlestick(market_type: MarketType) {\n    gen_test_crawl_candlestick!(EXCHANGE_NAME, market_type)\n}\n\n// #[test_case(MarketType::InverseFuture, \"BTCUSDZ22\")]\n// #[test_case(MarketType::InverseSwap, \"BTCUSD\")]\n// #[test_case(MarketType::LinearSwap, \"BTCUSDT\")]\n// fn test_subscribe_symbol(market_type: MarketType, symbol: &str) {\n//     gen_test_subscribe_symbol!(EXCHANGE_NAME, market_type, symbol)\n// }\n"
  },
  {
    "path": "crypto-crawler/tests/coinbase_pro.rs",
    "content": "#[macro_use]\nmod utils;\n\nuse test_case::test_case;\n\nuse crypto_crawler::*;\nuse crypto_market_type::MarketType;\nuse crypto_msg_type::MessageType;\nuse utils::parse;\n\nconst EXCHANGE_NAME: &str = \"coinbase_pro\";\n\n#[test_case(MarketType::Spot)]\n#[tokio::test(flavor = \"multi_thread\", worker_threads = 8)]\nasync fn test_crawl_trade_all(market_type: MarketType) {\n    test_all_symbols!(crawl_trade, EXCHANGE_NAME, market_type, MessageType::Trade)\n}\n\n#[test_case(MarketType::Spot, \"BTC-USD\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_trade(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_trade, EXCHANGE_NAME, market_type, symbol, MessageType::Trade)\n}\n\n#[test_case(MarketType::Spot, \"BTC-USD\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_l2_event(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_l2_event, EXCHANGE_NAME, market_type, symbol, MessageType::L2Event)\n}\n\n#[test_case(MarketType::Spot, \"BTC-USD\")]\nfn test_crawl_l2_snapshot(market_type: MarketType, symbol: &str) {\n    test_crawl_restful!(\n        crawl_l2_snapshot,\n        EXCHANGE_NAME,\n        market_type,\n        symbol,\n        MessageType::L2Snapshot\n    )\n}\n\n#[test_case(MarketType::Spot)]\nfn test_crawl_l2_snapshot_without_symbol(market_type: MarketType) {\n    test_crawl_restful_all_symbols!(\n        crawl_l2_snapshot,\n        EXCHANGE_NAME,\n        market_type,\n        MessageType::L2Snapshot\n    )\n}\n\n#[test_case(MarketType::Spot, \"BTC-USD\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_l3_event(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_l3_event, EXCHANGE_NAME, market_type, symbol, MessageType::L3Event)\n}\n\n#[test_case(MarketType::Spot, \"BTC-USD\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_l3_snapshot(market_type: MarketType, symbol: &str) {\n    test_crawl_restful!(\n        crawl_l3_snapshot,\n        EXCHANGE_NAME,\n        market_type,\n        symbol,\n        MessageType::L3Snapshot\n    )\n}\n\n#[test_case(MarketType::Spot, \"BTC-USD\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_ticker(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_ticker, EXCHANGE_NAME, market_type, symbol, MessageType::Ticker)\n}\n"
  },
  {
    "path": "crypto-crawler/tests/deribit.rs",
    "content": "#[macro_use]\nmod utils;\n\nuse test_case::test_case;\n\nuse crypto_crawler::*;\nuse crypto_market_type::MarketType;\nuse crypto_msg_type::MessageType;\nuse utils::parse;\n\nconst EXCHANGE_NAME: &str = \"deribit\";\n\n#[test_case(MarketType::InverseSwap)]\n#[test_case(MarketType::InverseFuture)]\n#[test_case(MarketType::EuropeanOption)]\n#[tokio::test(flavor = \"multi_thread\", worker_threads = 8)]\nasync fn test_crawl_trade_all(market_type: MarketType) {\n    test_all_symbols!(crawl_trade, EXCHANGE_NAME, market_type, MessageType::Trade)\n}\n\n#[test_case(MarketType::InverseSwap, \"BTC-PERPETUAL\")]\n// #[test_case(MarketType::InverseFuture, \"BTC-30DEC22\")]\n// #[test_case(MarketType::EuropeanOption, \"BTC-30DEC22-25000-C\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_trade(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_trade, EXCHANGE_NAME, market_type, symbol, MessageType::Trade)\n}\n\n#[test_case(MarketType::InverseSwap, \"BTC-PERPETUAL\")]\n#[test_case(MarketType::InverseFuture, \"BTC-30DEC22\")]\n#[test_case(MarketType::EuropeanOption, \"BTC-30DEC22-25000-C\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_l2_event(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_l2_event, EXCHANGE_NAME, market_type, symbol, MessageType::L2Event)\n}\n\n#[test_case(MarketType::InverseSwap, \"BTC-PERPETUAL\")]\n#[test_case(MarketType::InverseFuture, \"BTC-30DEC22\")]\n#[test_case(MarketType::EuropeanOption, \"BTC-30DEC22-25000-C\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_bbo(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_bbo, EXCHANGE_NAME, market_type, symbol, MessageType::BBO)\n}\n\n#[test_case(MarketType::InverseSwap, \"BTC-PERPETUAL\")]\n#[test_case(MarketType::InverseFuture, \"BTC-30DEC22\")]\n#[test_case(MarketType::EuropeanOption, \"BTC-30DEC22-25000-C\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_l2_topk(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_l2_topk, EXCHANGE_NAME, market_type, symbol, MessageType::L2TopK)\n}\n\n#[test_case(MarketType::InverseSwap, \"BTC-PERPETUAL\")]\n#[test_case(MarketType::InverseFuture, \"BTC-30DEC22\")]\n#[test_case(MarketType::EuropeanOption, \"BTC-30DEC22-25000-C\")]\nfn test_crawl_l2_snapshot(market_type: MarketType, symbol: &str) {\n    test_crawl_restful!(\n        crawl_l2_snapshot,\n        EXCHANGE_NAME,\n        market_type,\n        symbol,\n        MessageType::L2Snapshot\n    )\n}\n\n#[test_case(MarketType::InverseSwap)]\n#[test_case(MarketType::InverseFuture)]\n#[test_case(MarketType::EuropeanOption; \"inconclusive\")]\nfn test_crawl_l2_snapshot_without_symbol(market_type: MarketType) {\n    test_crawl_restful_all_symbols!(\n        crawl_l2_snapshot,\n        EXCHANGE_NAME,\n        market_type,\n        MessageType::L2Snapshot\n    )\n}\n\n#[test_case(MarketType::InverseSwap, \"BTC-PERPETUAL\")]\n#[test_case(MarketType::InverseFuture, \"BTC-30DEC22\")]\n#[test_case(MarketType::EuropeanOption, \"BTC-30DEC22-25000-C\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_ticker(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_ticker, EXCHANGE_NAME, market_type, symbol, MessageType::Ticker)\n}\n\n#[test_case(MarketType::InverseSwap)]\n#[test_case(MarketType::InverseFuture)]\n#[test_case(MarketType::EuropeanOption)]\n#[tokio::test(flavor = \"multi_thread\", worker_threads = 8)]\nasync fn test_crawl_candlestick(market_type: MarketType) {\n    gen_test_crawl_candlestick!(EXCHANGE_NAME, market_type)\n}\n\n// #[test_case(MarketType::InverseSwap, \"BTC-PERPETUAL\")]\n// #[test_case(MarketType::InverseFuture, \"BTC-30DEC22\")]\n// #[test_case(MarketType::EuropeanOption, \"BTC-30DEC22-25000-C\")]\n// fn test_subscribe_symbol(market_type: MarketType, symbol: &str) {\n//     gen_test_subscribe_symbol!(EXCHANGE_NAME, market_type, symbol)\n// }\n"
  },
  {
    "path": "crypto-crawler/tests/dydx.rs",
    "content": "#[macro_use]\nmod utils;\n\nuse test_case::test_case;\n\nuse crypto_crawler::*;\nuse crypto_market_type::MarketType;\nuse crypto_msg_type::MessageType;\nuse utils::parse;\n\nconst EXCHANGE_NAME: &str = \"dydx\";\n\n#[test_case(MarketType::LinearSwap)]\n#[tokio::test(flavor = \"multi_thread\", worker_threads = 8)]\nasync fn test_crawl_trade_all(market_type: MarketType) {\n    test_all_symbols!(crawl_trade, EXCHANGE_NAME, market_type, MessageType::Trade)\n}\n\n#[test_case(MarketType::LinearSwap, \"BTC-USD\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_trade(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_trade, EXCHANGE_NAME, market_type, symbol, MessageType::Trade)\n}\n\n#[test_case(MarketType::LinearSwap, \"BTC-USD\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_l2_event(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_l2_event, EXCHANGE_NAME, market_type, symbol, MessageType::L2Event)\n}\n\n#[test_case(MarketType::LinearSwap)]\nfn test_crawl_l2_snapshot_without_symbol(market_type: MarketType) {\n    test_crawl_restful_all_symbols!(\n        crawl_l2_snapshot,\n        EXCHANGE_NAME,\n        market_type,\n        MessageType::L2Snapshot\n    )\n}\n"
  },
  {
    "path": "crypto-crawler/tests/ftx.rs",
    "content": "#[macro_use]\nmod utils;\n\nuse test_case::test_case;\n\nuse crypto_crawler::*;\nuse crypto_market_type::MarketType;\nuse crypto_msg_type::MessageType;\nuse utils::parse;\n\nconst EXCHANGE_NAME: &str = \"ftx\";\n\n#[test_case(MarketType::Spot)]\n#[test_case(MarketType::LinearSwap)]\n#[test_case(MarketType::LinearFuture)]\n// #[test_case(MarketType::Move)]\n// #[test_case(MarketType::BVOL)]\n#[tokio::test(flavor = \"multi_thread\", worker_threads = 8)]\nasync fn test_crawl_trade_all(market_type: MarketType) {\n    test_all_symbols!(crawl_trade, EXCHANGE_NAME, market_type, MessageType::Trade)\n}\n\n#[test_case(MarketType::Spot, \"BTC/USD\")]\n#[test_case(MarketType::LinearSwap, \"BTC-PERP\")]\n#[test_case(MarketType::LinearFuture, \"BTC-1230\")]\n// #[test_case(MarketType::Move, \"BTC-MOVE-2022Q4\")]\n// #[test_case(MarketType::BVOL, \"BVOL/USD\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_trade(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_trade, EXCHANGE_NAME, market_type, symbol, MessageType::Trade)\n}\n\n#[test_case(MarketType::Spot, \"BTC/USD\")]\n#[test_case(MarketType::LinearSwap, \"BTC-PERP\")]\n#[test_case(MarketType::LinearFuture, \"BTC-1230\")]\n#[test_case(MarketType::Move, \"BTC-MOVE-2022Q4\")]\n#[test_case(MarketType::BVOL, \"BVOL/USD\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_l2_event(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_l2_event, EXCHANGE_NAME, market_type, symbol, MessageType::L2Event)\n}\n\n#[test_case(MarketType::Spot, \"BTC/USD\")]\n#[test_case(MarketType::LinearSwap, \"BTC-PERP\")]\n#[test_case(MarketType::LinearFuture, \"BTC-1230\")]\n#[test_case(MarketType::Move, \"BTC-MOVE-2022Q4\")]\n#[test_case(MarketType::BVOL, \"BVOL/USD\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_bbo(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_bbo, EXCHANGE_NAME, market_type, symbol, MessageType::BBO)\n}\n\n#[test_case(MarketType::Spot, \"BTC/USD\")]\n#[test_case(MarketType::LinearSwap, \"BTC-PERP\")]\n#[test_case(MarketType::LinearFuture, \"BTC-1230\")]\n#[test_case(MarketType::Move, \"BTC-MOVE-2022Q4\")]\n#[test_case(MarketType::BVOL, \"BVOL/USD\")]\nfn test_crawl_l2_snapshot(market_type: MarketType, symbol: &str) {\n    test_crawl_restful!(\n        crawl_l2_snapshot,\n        EXCHANGE_NAME,\n        market_type,\n        symbol,\n        MessageType::L2Snapshot\n    )\n}\n\n#[test_case(MarketType::Spot)]\n#[test_case(MarketType::LinearSwap)]\n#[test_case(MarketType::LinearFuture)]\n#[test_case(MarketType::Move)]\n#[test_case(MarketType::BVOL)]\nfn test_crawl_l2_snapshot_without_symbol(market_type: MarketType) {\n    test_crawl_restful_all_symbols!(\n        crawl_l2_snapshot,\n        EXCHANGE_NAME,\n        market_type,\n        MessageType::L2Snapshot\n    )\n}\n\n// #[test_case(MarketType::Spot, \"BTC/USD\")]\n// #[test_case(MarketType::LinearSwap, \"BTC-PERP\")]\n// #[test_case(MarketType::LinearFuture, \"BTC-1230\")]\n// fn test_subscribe_symbol(market_type: MarketType, symbol: &str) {\n//     gen_test_subscribe_symbol!(EXCHANGE_NAME, market_type, symbol)\n// }\n"
  },
  {
    "path": "crypto-crawler/tests/gate.rs",
    "content": "#[macro_use]\nmod utils;\n\nuse test_case::test_case;\n\nuse crypto_crawler::*;\nuse crypto_market_type::MarketType;\nuse crypto_msg_type::MessageType;\nuse utils::parse;\n\nconst EXCHANGE_NAME: &str = \"gate\";\n\n#[test_case(MarketType::Spot)]\n#[test_case(MarketType::InverseSwap)]\n#[test_case(MarketType::LinearSwap)]\n// #[test_case(MarketType::InverseFuture)]\n// #[test_case(MarketType::LinearFuture)]\n#[tokio::test(flavor = \"multi_thread\", worker_threads = 8)]\nasync fn test_crawl_trade_all(market_type: MarketType) {\n    test_all_symbols!(crawl_trade, EXCHANGE_NAME, market_type, MessageType::Trade)\n}\n\n#[test_case(MarketType::Spot, \"BTC_USDT\")]\n// #[test_case(MarketType::InverseSwap, \"BTC_USD\")]\n#[test_case(MarketType::LinearSwap, \"BTC_USDT\")]\n// #[test_case(MarketType::InverseFuture, \"BTC_USD_20221230\"; \"ignore\")]\n// #[test_case(MarketType::LinearFuture, \"BTC_USDT_20221230\"; \"ignore\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_trade(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_trade, EXCHANGE_NAME, market_type, symbol, MessageType::Trade)\n}\n\n#[test_case(MarketType::Spot, \"BTC_USDT\")]\n#[test_case(MarketType::InverseSwap, \"BTC_USD\")]\n#[test_case(MarketType::LinearSwap, \"BTC_USDT\")]\n#[test_case(MarketType::InverseFuture, \"BTC_USD_20221230\")]\n#[test_case(MarketType::LinearFuture, \"BTC_USDT_20221230\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_l2_event(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_l2_event, EXCHANGE_NAME, market_type, symbol, MessageType::L2Event)\n}\n\n#[test_case(MarketType::Spot, \"BTC_USDT\")]\n#[test_case(MarketType::InverseSwap, \"BTC_USD\")]\n#[test_case(MarketType::LinearSwap, \"BTC_USDT\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_bbo(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_bbo, EXCHANGE_NAME, market_type, symbol, MessageType::BBO)\n}\n\n#[test_case(MarketType::Spot, \"BTC_USDT\")]\n#[test_case(MarketType::InverseSwap, \"BTC_USD\")]\n#[test_case(MarketType::LinearSwap, \"BTC_USDT\")]\n#[test_case(MarketType::InverseFuture, \"BTC_USD_20221230\")]\n#[test_case(MarketType::LinearFuture, \"BTC_USDT_20221230\")]\nfn test_crawl_l2_snapshot(market_type: MarketType, symbol: &str) {\n    test_crawl_restful!(\n        crawl_l2_snapshot,\n        EXCHANGE_NAME,\n        market_type,\n        symbol,\n        MessageType::L2Snapshot\n    )\n}\n\n#[test_case(MarketType::Spot)]\n#[test_case(MarketType::InverseSwap)]\n#[test_case(MarketType::LinearSwap)]\n#[test_case(MarketType::InverseFuture)]\n#[test_case(MarketType::LinearFuture)]\nfn test_crawl_l2_snapshot_without_symbol(market_type: MarketType) {\n    test_crawl_restful_all_symbols!(\n        crawl_l2_snapshot,\n        EXCHANGE_NAME,\n        market_type,\n        MessageType::L2Snapshot\n    )\n}\n\n#[test_case(MarketType::Spot, \"BTC_USDT\")]\n#[test_case(MarketType::InverseSwap, \"BTC_USD\")]\n#[test_case(MarketType::LinearSwap, \"BTC_USDT\")]\n// #[test_case(MarketType::InverseFuture, \"BTC_USD_20221230\")]\n#[test_case(MarketType::LinearFuture, \"BTC_USDT_20221230\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_ticker(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_ticker, EXCHANGE_NAME, market_type, symbol, MessageType::Ticker)\n}\n\n// #[test_case(MarketType::Spot; \"inconclusive due to too many symbols\")]\n#[test_case(MarketType::InverseSwap)]\n// #[test_case(MarketType::LinearSwap)] // always timeout in Github workflow\n// #[test_case(MarketType::InverseFuture)]\n// #[test_case(MarketType::LinearFuture)]\n#[tokio::test(flavor = \"multi_thread\", worker_threads = 8)]\nasync fn test_crawl_candlestick(market_type: MarketType) {\n    gen_test_crawl_candlestick!(EXCHANGE_NAME, market_type)\n}\n"
  },
  {
    "path": "crypto-crawler/tests/huobi.rs",
    "content": "#[macro_use]\nmod utils;\n\nuse test_case::test_case;\n\nuse crypto_crawler::*;\nuse crypto_market_type::MarketType;\nuse crypto_msg_type::MessageType;\nuse utils::parse;\n\nconst EXCHANGE_NAME: &str = \"huobi\";\n\n#[test_case(MarketType::Spot)]\n#[test_case(MarketType::InverseFuture)]\n#[test_case(MarketType::InverseSwap)]\n#[test_case(MarketType::LinearSwap)]\n#[tokio::test(flavor = \"multi_thread\", worker_threads = 8)]\nasync fn test_crawl_trade_all(market_type: MarketType) {\n    test_all_symbols!(crawl_trade, EXCHANGE_NAME, market_type, MessageType::Trade)\n}\n\n#[test_case(MarketType::Spot, \"btcusdt\")]\n#[test_case(MarketType::InverseFuture, \"BTC_CQ\")]\n#[test_case(MarketType::InverseSwap, \"BTC-USD\")]\n#[test_case(MarketType::LinearSwap, \"BTC-USDT\")]\n#[test_case(MarketType::EuropeanOption, \"BTC-USDT-210625-P-27000\"; \"inconclusive\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_trade(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_trade, EXCHANGE_NAME, market_type, symbol, MessageType::Trade)\n}\n\n#[test_case(MarketType::Spot, \"btcusdt\")]\n#[test_case(MarketType::InverseFuture, \"BTC_CQ\")]\n#[test_case(MarketType::InverseSwap, \"BTC-USD\")]\n#[test_case(MarketType::LinearSwap, \"BTC-USDT\")]\n#[test_case(MarketType::EuropeanOption, \"BTC-USDT-210625-P-27000\"; \"inconclusive\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_l2_event(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_l2_event, EXCHANGE_NAME, market_type, symbol, MessageType::L2Event)\n}\n\n#[test_case(MarketType::Spot, \"btcusdt\")]\n#[test_case(MarketType::InverseFuture, \"BTC_CQ\")]\n#[test_case(MarketType::InverseSwap, \"BTC-USD\")]\n#[test_case(MarketType::LinearSwap, \"BTC-USDT\")]\n#[test_case(MarketType::EuropeanOption, \"BTC-USDT-210625-P-27000\"; \"inconclusive\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_bbo(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_bbo, EXCHANGE_NAME, market_type, symbol, MessageType::BBO)\n}\n\n#[test_case(MarketType::Spot, \"btcusdt\")]\n#[test_case(MarketType::InverseFuture, \"BTC_CQ\")]\n#[test_case(MarketType::InverseSwap, \"BTC-USD\")]\n#[test_case(MarketType::LinearSwap, \"BTC-USDT\")]\n#[test_case(MarketType::EuropeanOption, \"BTC-USDT-210625-P-27000\"; \"inconclusive\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_l2_topk(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_l2_topk, EXCHANGE_NAME, market_type, symbol, MessageType::L2TopK)\n}\n\n#[test_case(MarketType::Spot, \"btcusdt\")]\n#[test_case(MarketType::InverseFuture, \"BTC_CQ\")]\n#[test_case(MarketType::InverseSwap, \"BTC-USD\")]\n#[test_case(MarketType::LinearSwap, \"BTC-USDT\")]\n#[test_case(MarketType::EuropeanOption, \"BTC-USDT-210625-P-27000\"; \"inconclusive\")]\nfn test_crawl_l2_snapshot(market_type: MarketType, symbol: &str) {\n    test_crawl_restful!(\n        crawl_l2_snapshot,\n        EXCHANGE_NAME,\n        market_type,\n        symbol,\n        MessageType::L2Snapshot\n    )\n}\n\n#[test_case(MarketType::Spot)]\n#[test_case(MarketType::InverseFuture)]\n#[test_case(MarketType::InverseSwap)]\n#[test_case(MarketType::LinearSwap)]\n#[test_case(MarketType::EuropeanOption; \"inconclusive\")]\nfn test_crawl_l2_snapshot_without_symbol(market_type: MarketType) {\n    test_crawl_restful_all_symbols!(\n        crawl_l2_snapshot,\n        EXCHANGE_NAME,\n        market_type,\n        MessageType::L2Snapshot\n    )\n}\n\n#[test_case(MarketType::InverseSwap, \"BTC-USD\")]\n#[test_case(MarketType::LinearSwap, \"BTC-USDT\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_funding_rate(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(\n        crawl_funding_rate,\n        EXCHANGE_NAME,\n        market_type,\n        symbol,\n        MessageType::FundingRate\n    )\n}\n\n#[test_case(MarketType::Spot, \"btcusdt\")]\n#[test_case(MarketType::InverseFuture, \"BTC_CQ\")]\n#[test_case(MarketType::InverseSwap, \"BTC-USD\")]\n#[test_case(MarketType::LinearSwap, \"BTC-USDT\")]\n#[test_case(MarketType::EuropeanOption, \"BTC-USDT-210625-P-27000\"; \"inconclusive\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_ticker(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_ticker, EXCHANGE_NAME, market_type, symbol, MessageType::Ticker)\n}\n\n#[test_case(MarketType::Spot)]\n#[test_case(MarketType::InverseFuture)]\n#[test_case(MarketType::InverseSwap)]\n#[test_case(MarketType::LinearSwap)]\n#[test_case(MarketType::EuropeanOption; \"inconclusive\")]\n#[tokio::test(flavor = \"multi_thread\", worker_threads = 8)]\nasync fn test_crawl_candlestick(market_type: MarketType) {\n    gen_test_crawl_candlestick!(EXCHANGE_NAME, market_type)\n}\n\n// #[test_case(MarketType::Spot, \"btcusdt\")]\n// #[test_case(MarketType::InverseFuture, \"BTC_CQ\")]\n// #[test_case(MarketType::InverseSwap, \"BTC-USD\")]\n// #[test_case(MarketType::LinearSwap, \"BTC-USDT\")]\n// fn test_subscribe_symbol(market_type: MarketType, symbol: &str) {\n//     gen_test_subscribe_symbol!(EXCHANGE_NAME, market_type, symbol)\n// }\n"
  },
  {
    "path": "crypto-crawler/tests/kraken.rs",
    "content": "#[macro_use]\nmod utils;\n\nuse test_case::test_case;\n\nuse crypto_crawler::*;\nuse crypto_market_type::MarketType;\nuse crypto_msg_type::MessageType;\nuse utils::parse;\n\nconst EXCHANGE_NAME: &str = \"kraken\";\n\n#[test_case(MarketType::Spot)]\n#[test_case(MarketType::InverseFuture)]\n#[test_case(MarketType::InverseSwap)]\n#[tokio::test(flavor = \"multi_thread\", worker_threads = 8)]\nasync fn test_crawl_trade_all(market_type: MarketType) {\n    test_all_symbols!(crawl_trade, EXCHANGE_NAME, market_type, MessageType::Trade)\n}\n\n#[test_case(MarketType::Spot, \"XBT/USD\")]\n#[test_case(MarketType::InverseFuture, \"FI_XBTUSD_221230\")]\n#[test_case(MarketType::InverseSwap, \"PI_XBTUSD\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_trade(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_trade, EXCHANGE_NAME, market_type, symbol, MessageType::Trade)\n}\n\n#[test_case(MarketType::Spot, \"XBT/USD\")]\n#[test_case(MarketType::InverseFuture, \"FI_XBTUSD_221230\")]\n#[test_case(MarketType::InverseSwap, \"PI_XBTUSD\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_l2_event(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_l2_event, EXCHANGE_NAME, market_type, symbol, MessageType::L2Event)\n}\n\n#[test_case(MarketType::Spot, \"XBT/USD\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_bbo(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_bbo, EXCHANGE_NAME, market_type, symbol, MessageType::BBO)\n}\n\n#[test_case(MarketType::Spot, \"XBT/USD\")]\n#[test_case(MarketType::InverseFuture, \"FI_XBTUSD_221230\")]\n#[test_case(MarketType::InverseSwap, \"PI_XBTUSD\")]\nfn test_crawl_l2_snapshot(market_type: MarketType, symbol: &str) {\n    test_crawl_restful!(\n        crawl_l2_snapshot,\n        EXCHANGE_NAME,\n        market_type,\n        symbol,\n        MessageType::L2Snapshot\n    )\n}\n\n#[test_case(MarketType::Spot)]\n#[test_case(MarketType::InverseFuture)]\n#[test_case(MarketType::InverseSwap)]\nfn test_crawl_l2_snapshot_without_symbol(market_type: MarketType) {\n    test_crawl_restful_all_symbols!(\n        crawl_l2_snapshot,\n        EXCHANGE_NAME,\n        market_type,\n        MessageType::L2Snapshot\n    )\n}\n\n#[test_case(MarketType::Spot, \"XBT/USD\")]\n#[test_case(MarketType::InverseFuture, \"FI_XBTUSD_221230\")]\n#[test_case(MarketType::InverseSwap, \"PI_XBTUSD\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_ticker(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_ticker, EXCHANGE_NAME, market_type, symbol, MessageType::Ticker)\n}\n\n#[test_case(MarketType::Spot)]\n#[tokio::test(flavor = \"multi_thread\", worker_threads = 8)]\nasync fn test_crawl_candlestick(market_type: MarketType) {\n    gen_test_crawl_candlestick!(EXCHANGE_NAME, market_type)\n}\n"
  },
  {
    "path": "crypto-crawler/tests/kucoin.rs",
    "content": "#[macro_use]\nmod utils;\n\nuse test_case::test_case;\n\nuse crypto_crawler::*;\nuse crypto_market_type::MarketType;\nuse crypto_msg_type::MessageType;\nuse utils::parse;\n\nconst EXCHANGE_NAME: &str = \"kucoin\";\n\n#[test_case(MarketType::Spot)]\n#[test_case(MarketType::InverseSwap)]\n#[test_case(MarketType::LinearSwap)]\n#[tokio::test(flavor = \"multi_thread\", worker_threads = 8)]\nasync fn test_crawl_trade_all(market_type: MarketType) {\n    test_all_symbols!(crawl_trade, EXCHANGE_NAME, market_type, MessageType::Trade)\n}\n\n#[test_case(MarketType::Spot, \"BTC-USDT\")]\n// #[test_case(MarketType::InverseSwap, \"XBTUSDM\")]\n#[test_case(MarketType::LinearSwap, \"XBTUSDTM\")]\n// #[test_case(MarketType::InverseFuture, \"XBTMZ22\"; \"ignore\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_trade(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_trade, EXCHANGE_NAME, market_type, symbol, MessageType::Trade)\n}\n\n#[test_case(MarketType::Spot, \"BTC-USDT\")]\n#[test_case(MarketType::InverseSwap, \"XBTUSDM\")]\n#[test_case(MarketType::LinearSwap, \"XBTUSDTM\")]\n#[test_case(MarketType::InverseFuture, \"XBTMZ22\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_l2_event(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_l2_event, EXCHANGE_NAME, market_type, symbol, MessageType::L2Event)\n}\n\n#[test_case(MarketType::Spot, \"BTC-USDT\")]\n#[test_case(MarketType::InverseSwap, \"XBTUSDM\")]\n#[test_case(MarketType::LinearSwap, \"XBTUSDTM\")]\n#[test_case(MarketType::InverseFuture, \"XBTMZ22\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_bbo(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_bbo, EXCHANGE_NAME, market_type, symbol, MessageType::BBO)\n}\n\n#[test_case(MarketType::Spot, \"BTC-USDT\")]\n#[test_case(MarketType::InverseSwap, \"XBTUSDM\")]\n#[test_case(MarketType::LinearSwap, \"XBTUSDTM\")]\n#[test_case(MarketType::InverseFuture, \"XBTMZ22\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_l2_topk(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_l2_topk, EXCHANGE_NAME, market_type, symbol, MessageType::L2TopK)\n}\n\n#[test_case(MarketType::Spot, \"BTC-USDT\")]\n#[test_case(MarketType::InverseSwap, \"XBTUSDM\")]\n#[test_case(MarketType::LinearSwap, \"XBTUSDTM\")]\n#[test_case(MarketType::InverseFuture, \"XBTMZ22\")]\nfn test_crawl_l2_snapshot(market_type: MarketType, symbol: &str) {\n    test_crawl_restful!(\n        crawl_l2_snapshot,\n        EXCHANGE_NAME,\n        market_type,\n        symbol,\n        MessageType::L2Snapshot\n    )\n}\n\n#[test_case(MarketType::Spot)]\n#[test_case(MarketType::InverseSwap)]\n#[test_case(MarketType::LinearSwap)]\n#[test_case(MarketType::InverseFuture)]\nfn test_crawl_l2_snapshot_without_symbol(market_type: MarketType) {\n    test_crawl_restful_all_symbols!(\n        crawl_l2_snapshot,\n        EXCHANGE_NAME,\n        market_type,\n        MessageType::L2Snapshot\n    )\n}\n\n// #[test_case(MarketType::Spot, \"BTC-USDT\"; \"ignore\")]\n#[test_case(MarketType::InverseSwap, \"XBTUSDM\")]\n#[test_case(MarketType::LinearSwap, \"XBTUSDTM\")]\n#[test_case(MarketType::InverseFuture, \"XBTMZ22\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_l3_snapshot(market_type: MarketType, symbol: &str) {\n    test_crawl_restful!(\n        crawl_l3_snapshot,\n        EXCHANGE_NAME,\n        market_type,\n        symbol,\n        MessageType::L3Snapshot\n    )\n}\n\n#[test_case(MarketType::Spot, \"BTC-USDT\")]\n#[test_case(MarketType::InverseSwap, \"XBTUSDM\")]\n#[test_case(MarketType::LinearSwap, \"XBTUSDTM\")]\n#[test_case(MarketType::InverseFuture, \"XBTMZ22\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_ticker(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_ticker, EXCHANGE_NAME, market_type, symbol, MessageType::Ticker)\n}\n\n#[test_case(MarketType::Spot)]\n#[test_case(MarketType::InverseSwap)]\n#[test_case(MarketType::LinearSwap)]\n// #[test_case(MarketType::InverseFuture)]\n#[tokio::test(flavor = \"multi_thread\", worker_threads = 8)]\nasync fn test_crawl_candlestick(market_type: MarketType) {\n    gen_test_crawl_candlestick!(EXCHANGE_NAME, market_type)\n}\n"
  },
  {
    "path": "crypto-crawler/tests/mexc.rs",
    "content": "#[macro_use]\nmod utils;\n\nuse test_case::test_case;\n\nuse crypto_crawler::*;\nuse crypto_market_type::MarketType;\nuse crypto_msg_type::MessageType;\nuse utils::parse;\n\nconst EXCHANGE_NAME: &str = \"mexc\";\n\n#[test_case(MarketType::Spot)]\n#[test_case(MarketType::InverseSwap)]\n#[test_case(MarketType::LinearSwap)]\n#[tokio::test(flavor = \"multi_thread\", worker_threads = 8)]\nasync fn test_crawl_trade_all(market_type: MarketType) {\n    test_all_symbols!(crawl_trade, EXCHANGE_NAME, market_type, MessageType::Trade)\n}\n\n#[test_case(MarketType::Spot, \"BTC_USDT\")]\n#[test_case(MarketType::LinearSwap, \"BTC_USDT\")]\n#[test_case(MarketType::InverseSwap, \"BTC_USD\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_trade(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_trade, EXCHANGE_NAME, market_type, symbol, MessageType::Trade)\n}\n\n#[test_case(MarketType::Spot, \"BTC_USDT\")]\n#[test_case(MarketType::LinearSwap, \"BTC_USDT\")]\n#[test_case(MarketType::InverseSwap, \"BTC_USD\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_l2_event(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_l2_event, EXCHANGE_NAME, market_type, symbol, MessageType::L2Event)\n}\n\n#[test_case(MarketType::Spot, \"BTC_USDT\")]\n#[test_case(MarketType::LinearSwap, \"BTC_USDT\")]\n#[test_case(MarketType::InverseSwap, \"BTC_USD\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_l2_topk(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_l2_topk, EXCHANGE_NAME, market_type, symbol, MessageType::L2TopK)\n}\n\n#[test_case(MarketType::Spot, \"BTC_USDT\")]\n#[test_case(MarketType::LinearSwap, \"BTC_USDT\")]\n#[test_case(MarketType::InverseSwap, \"BTC_USD\")]\nfn test_crawl_l2_snapshot(market_type: MarketType, symbol: &str) {\n    test_crawl_restful!(\n        crawl_l2_snapshot,\n        EXCHANGE_NAME,\n        market_type,\n        symbol,\n        MessageType::L2Snapshot\n    )\n}\n\n#[test_case(MarketType::Spot)]\n#[test_case(MarketType::LinearSwap)]\n#[test_case(MarketType::InverseSwap)]\nfn test_crawl_l2_snapshot_without_symbol(market_type: MarketType) {\n    test_crawl_restful_all_symbols!(\n        crawl_l2_snapshot,\n        EXCHANGE_NAME,\n        market_type,\n        MessageType::L2Snapshot\n    )\n}\n\n#[test_case(MarketType::LinearSwap, \"BTC_USDT\")]\n#[test_case(MarketType::InverseSwap, \"BTC_USD\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_ticker(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_ticker, EXCHANGE_NAME, market_type, symbol, MessageType::Ticker)\n}\n\n#[test_case(MarketType::Spot)]\n#[test_case(MarketType::LinearSwap)]\n#[test_case(MarketType::InverseSwap)]\n#[tokio::test(flavor = \"multi_thread\", worker_threads = 8)]\nasync fn test_crawl_candlestick(market_type: MarketType) {\n    gen_test_crawl_candlestick!(EXCHANGE_NAME, market_type)\n}\n"
  },
  {
    "path": "crypto-crawler/tests/okx.rs",
    "content": "#[macro_use]\nmod utils;\n\nuse test_case::test_case;\n\nuse crypto_crawler::*;\nuse crypto_market_type::MarketType;\nuse crypto_msg_type::MessageType;\nuse utils::parse;\n\nconst EXCHANGE_NAME: &str = \"okx\";\n\n#[test_case(MarketType::Spot)]\n#[test_case(MarketType::InverseFuture)]\n#[test_case(MarketType::LinearFuture)]\n#[test_case(MarketType::InverseSwap)]\n#[test_case(MarketType::LinearSwap)]\n#[test_case(MarketType::EuropeanOption)]\n#[tokio::test(flavor = \"multi_thread\", worker_threads = 8)]\nasync fn test_crawl_trade_all(market_type: MarketType) {\n    test_all_symbols!(crawl_trade, EXCHANGE_NAME, market_type, MessageType::Trade)\n}\n\n#[test_case(MarketType::Spot, \"BTC-USDT\")]\n#[test_case(MarketType::InverseFuture, \"BTC-USD-221230\")]\n#[test_case(MarketType::LinearFuture, \"BTC-USDT-221230\")]\n#[test_case(MarketType::InverseSwap, \"BTC-USD-SWAP\")]\n#[test_case(MarketType::LinearSwap, \"BTC-USDT-SWAP\")]\n// #[test_case(MarketType::EuropeanOption, \"BTC-USD-221230-10000-P\"; \"ignore\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_trade(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_trade, EXCHANGE_NAME, market_type, symbol, MessageType::Trade)\n}\n\n#[test_case(MarketType::Spot, \"BTC-USDT\")]\n#[test_case(MarketType::InverseFuture, \"BTC-USD-221230\")]\n#[test_case(MarketType::LinearFuture, \"BTC-USDT-221230\")]\n#[test_case(MarketType::InverseSwap, \"BTC-USD-SWAP\")]\n#[test_case(MarketType::LinearSwap, \"BTC-USDT-SWAP\")]\n#[test_case(MarketType::EuropeanOption, \"BTC-USD-221230-10000-P\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_l2_event(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_l2_event, EXCHANGE_NAME, market_type, symbol, MessageType::L2Event)\n}\n\n#[test_case(MarketType::Spot, \"BTC-USDT\")]\n#[test_case(MarketType::InverseFuture, \"BTC-USD-221230\")]\n#[test_case(MarketType::LinearFuture, \"BTC-USDT-221230\")]\n#[test_case(MarketType::InverseSwap, \"BTC-USD-SWAP\")]\n#[test_case(MarketType::LinearSwap, \"BTC-USDT-SWAP\")]\n#[test_case(MarketType::EuropeanOption, \"BTC-USD-221230-10000-P\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_l2_topk(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_l2_topk, EXCHANGE_NAME, market_type, symbol, MessageType::L2TopK)\n}\n\n#[test_case(MarketType::Spot, \"BTC-USDT\")]\n#[test_case(MarketType::InverseFuture, \"BTC-USD-221230\")]\n#[test_case(MarketType::LinearFuture, \"BTC-USDT-221230\")]\n#[test_case(MarketType::InverseSwap, \"BTC-USD-SWAP\")]\n#[test_case(MarketType::LinearSwap, \"BTC-USDT-SWAP\")]\n#[test_case(MarketType::EuropeanOption, \"BTC-USD-221230-10000-P\")]\nfn test_crawl_l2_snapshot(market_type: MarketType, symbol: &str) {\n    test_crawl_restful!(\n        crawl_l2_snapshot,\n        EXCHANGE_NAME,\n        market_type,\n        symbol,\n        MessageType::L2Snapshot\n    )\n}\n\n#[test_case(MarketType::Spot)]\n#[test_case(MarketType::InverseFuture)]\n#[test_case(MarketType::LinearFuture)]\n#[test_case(MarketType::InverseSwap)]\n#[test_case(MarketType::LinearSwap)]\n#[test_case(MarketType::EuropeanOption)]\nfn test_crawl_l2_snapshot_without_symbol(market_type: MarketType) {\n    test_crawl_restful_all_symbols!(\n        crawl_l2_snapshot,\n        EXCHANGE_NAME,\n        market_type,\n        MessageType::L2Snapshot\n    )\n}\n\n#[test_case(MarketType::InverseSwap, \"BTC-USD-SWAP\")]\n#[test_case(MarketType::LinearSwap, \"BTC-USDT-SWAP\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_funding_rate(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(\n        crawl_funding_rate,\n        EXCHANGE_NAME,\n        market_type,\n        symbol,\n        MessageType::FundingRate\n    )\n}\n\n#[test_case(MarketType::InverseFuture)]\n#[test_case(MarketType::LinearFuture)]\n#[test_case(MarketType::InverseSwap)]\n#[test_case(MarketType::LinearSwap)]\n#[test_case(MarketType::EuropeanOption)]\nfn test_crawl_open_interest(market_type: MarketType) {\n    let (tx, rx) = std::sync::mpsc::channel();\n    std::thread::spawn(move || {\n        crawl_open_interest(EXCHANGE_NAME, market_type, tx);\n    });\n\n    let msg = rx.recv().unwrap();\n\n    assert_eq!(msg.exchange, EXCHANGE_NAME.to_string());\n    assert_eq!(msg.market_type, market_type);\n    assert_eq!(msg.msg_type, MessageType::OpenInterest);\n\n    assert!(parse(msg));\n}\n\n#[test_case(MarketType::Spot, \"BTC-USDT\")]\n#[test_case(MarketType::InverseFuture, \"BTC-USD-221230\")]\n#[test_case(MarketType::LinearFuture, \"BTC-USDT-221230\")]\n#[test_case(MarketType::InverseSwap, \"BTC-USD-SWAP\")]\n#[test_case(MarketType::LinearSwap, \"BTC-USDT-SWAP\")]\n#[test_case(MarketType::EuropeanOption, \"BTC-USD-221230-10000-P\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_ticker(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_ticker, EXCHANGE_NAME, market_type, symbol, MessageType::Ticker)\n}\n\n#[test_case(MarketType::Spot)]\n#[test_case(MarketType::InverseFuture)]\n#[test_case(MarketType::LinearFuture)]\n#[test_case(MarketType::InverseSwap)]\n#[test_case(MarketType::LinearSwap)]\n#[test_case(MarketType::EuropeanOption)]\n#[tokio::test(flavor = \"multi_thread\", worker_threads = 8)]\nasync fn test_crawl_candlestick(market_type: MarketType) {\n    gen_test_crawl_candlestick!(EXCHANGE_NAME, market_type)\n}\n\n// #[test_case(MarketType::Spot, \"BTC-USDT\")]\n// #[test_case(MarketType::InverseFuture, \"BTC-USD-221230\")]\n// #[test_case(MarketType::LinearFuture, \"BTC-USDT-221230\")]\n// #[test_case(MarketType::InverseSwap, \"BTC-USD-SWAP\")]\n// #[test_case(MarketType::LinearSwap, \"BTC-USDT-SWAP\")]\n// #[test_case(MarketType::EuropeanOption, \"BTC-USD-221230-10000-P\")]\n// fn test_subscribe_symbol(market_type: MarketType, symbol: &str) {\n//     gen_test_subscribe_symbol!(EXCHANGE_NAME, market_type, symbol)\n// }\n"
  },
  {
    "path": "crypto-crawler/tests/utils/mod.rs",
    "content": "use crypto_crawler::Message;\nuse crypto_market_type::MarketType;\nuse crypto_msg_type::MessageType;\n\npub(crate) fn parse(msg: Message) -> bool {\n    let skipped_exchanges = vec![\"bitget\", \"zb\"];\n    if skipped_exchanges.contains(&msg.exchange.as_str()) {\n        return true;\n    }\n    match msg.msg_type {\n        MessageType::Trade => {\n            crypto_msg_parser::parse_trade(&msg.exchange, msg.market_type, &msg.json).is_ok()\n        }\n        MessageType::L2Event => {\n            match msg.market_type {\n                // crypto-msg-parser doesn't support quanto contracts\n                MarketType::QuantoSwap | MarketType::QuantoFuture => true,\n                _ => crypto_msg_parser::parse_l2(\n                    &msg.exchange,\n                    msg.market_type,\n                    &msg.json,\n                    Some(msg.received_at as i64),\n                )\n                .is_ok(),\n            }\n        }\n        MessageType::FundingRate => crypto_msg_parser::parse_funding_rate(\n            &msg.exchange,\n            msg.market_type,\n            &msg.json,\n            Some(msg.received_at as i64),\n        )\n        .is_ok(),\n        _ => true,\n    }\n}\n\n#[allow(unused_macros)]\nmacro_rules! test_one_symbol {\n    ($crawl_func:ident, $exchange:expr, $market_type:expr, $symbol:expr, $msg_type:expr) => {{\n        let (tx, rx) = std::sync::mpsc::channel();\n        let symbols = vec![$symbol.to_string()];\n        tokio::task::spawn(async move {\n            $crawl_func($exchange, $market_type, Some(&symbols), tx).await;\n        });\n\n        let msg = rx.recv().unwrap();\n\n        assert_eq!(msg.exchange, $exchange.to_string());\n        assert_eq!(msg.market_type, $market_type);\n        assert_eq!(msg.msg_type, $msg_type);\n\n        assert!(tokio::task::block_in_place(move || parse(msg)));\n    }};\n}\n\n#[allow(unused_macros)]\nmacro_rules! test_all_symbols {\n    ($crawl_func:ident, $exchange:expr, $market_type:expr, $msg_type:expr) => {{\n        let (tx, rx) = std::sync::mpsc::channel();\n        tokio::task::spawn(async move {\n            $crawl_func($exchange, $market_type, None, tx).await;\n        });\n\n        let msg = rx.recv().unwrap();\n\n        assert_eq!(msg.exchange, $exchange.to_string());\n        assert_eq!(msg.market_type, $market_type);\n        assert_eq!(msg.msg_type, $msg_type);\n\n        assert!(tokio::task::block_in_place(move || parse(msg)));\n    }};\n}\n\n#[allow(unused_macros)]\nmacro_rules! test_crawl_restful {\n    ($crawl_func:ident, $exchange:expr, $market_type:expr, $symbol:expr, $msg_type:expr) => {{\n        let (tx, rx) = std::sync::mpsc::channel();\n        let symbols = vec![$symbol.to_string()];\n        std::thread::spawn(move || {\n            $crawl_func($exchange, $market_type, Some(&symbols), tx);\n        });\n\n        let msg = rx.recv().unwrap();\n\n        assert_eq!(msg.exchange, $exchange.to_string());\n        assert_eq!(msg.market_type, $market_type);\n        assert_eq!(msg.msg_type, $msg_type);\n\n        assert!(parse(msg));\n    }};\n}\n\n#[allow(unused_macros)]\nmacro_rules! test_crawl_restful_all_symbols {\n    ($crawl_func:ident, $exchange:expr, $market_type:expr, $msg_type:expr) => {{\n        let (tx, rx) = std::sync::mpsc::channel();\n        std::thread::spawn(move || {\n            $crawl_func($exchange, $market_type, None, tx);\n        });\n\n        let msg = rx.recv().unwrap();\n\n        assert_eq!(msg.exchange, $exchange.to_string());\n        assert_eq!(msg.market_type, $market_type);\n        assert_eq!(msg.msg_type, $msg_type);\n\n        assert!(parse(msg));\n    }};\n}\n\n#[allow(unused_macros)]\nmacro_rules! gen_test_crawl_candlestick {\n    ($exchange:expr, $market_type:expr) => {{\n        let (tx, rx) = std::sync::mpsc::channel();\n        tokio::task::spawn(async move {\n            crawl_candlestick($exchange, $market_type, None, tx).await;\n        });\n\n        let msg = rx.recv().unwrap();\n\n        assert_eq!(msg.exchange, $exchange.to_string());\n        assert_eq!(msg.market_type, $market_type);\n        assert_eq!(msg.msg_type, MessageType::Candlestick);\n\n        assert!(tokio::task::block_in_place(move || parse(msg)));\n    }};\n}\n\n#[allow(unused_macros)]\nmacro_rules! gen_test_subscribe_symbol {\n    ($exchange:expr, $market_type:expr, $symbol:expr) => {{\n        let (tx, rx) = std::sync::mpsc::channel();\n        tokio::task::spawn(async move {\n            let msg_types = vec![MessageType::Trade, MessageType::L2Event];\n            subscribe_symbol($exchange, $market_type, $symbol, &msg_types, tx).await;\n        });\n\n        let mut messages = Vec::new();\n        for msg in rx {\n            messages.push(msg);\n            break;\n        }\n        assert!(!messages.is_empty());\n    }};\n}\n"
  },
  {
    "path": "crypto-crawler/tests/zb.rs",
    "content": "#[macro_use]\nmod utils;\n\nuse test_case::test_case;\n\nuse crypto_crawler::*;\nuse crypto_market_type::MarketType;\nuse crypto_msg_type::MessageType;\nuse utils::parse;\n\nconst EXCHANGE_NAME: &str = \"zb\";\n\n#[test_case(MarketType::Spot)]\n#[test_case(MarketType::LinearSwap)]\n#[tokio::test(flavor = \"multi_thread\", worker_threads = 8)]\nasync fn test_crawl_trade_all(market_type: MarketType) {\n    test_all_symbols!(crawl_trade, EXCHANGE_NAME, market_type, MessageType::Trade)\n}\n\n#[test_case(MarketType::Spot, \"btc_usdt\")]\n#[test_case(MarketType::LinearSwap, \"BTC_USDT\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_trade(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_trade, EXCHANGE_NAME, market_type, symbol, MessageType::Trade)\n}\n\n// #[test_case(MarketType::Spot, \"btc_usdt\")]\n#[test_case(MarketType::LinearSwap, \"BTC_USDT\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_l2_event(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_l2_event, EXCHANGE_NAME, market_type, symbol, MessageType::L2Event)\n}\n\n#[test_case(MarketType::Spot, \"btc_usdt\")]\n#[test_case(MarketType::LinearSwap, \"BTC_USDT\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_l2_topk(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_l2_topk, EXCHANGE_NAME, market_type, symbol, MessageType::L2TopK)\n}\n\n#[test_case(MarketType::Spot, \"btc_usdt\")]\n#[test_case(MarketType::LinearSwap, \"BTC_USDT\")]\nfn test_crawl_l2_snapshot(market_type: MarketType, symbol: &str) {\n    test_crawl_restful!(\n        crawl_l2_snapshot,\n        EXCHANGE_NAME,\n        market_type,\n        symbol,\n        MessageType::L2Snapshot\n    )\n}\n\n#[test_case(MarketType::Spot)]\n#[test_case(MarketType::LinearSwap)]\nfn test_crawl_l2_snapshot_without_symbol(market_type: MarketType) {\n    test_crawl_restful_all_symbols!(\n        crawl_l2_snapshot,\n        EXCHANGE_NAME,\n        market_type,\n        MessageType::L2Snapshot\n    )\n}\n\n#[test_case(MarketType::Spot, \"btc_usdt\")]\n#[test_case(MarketType::LinearSwap, \"BTC_USDT\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_ticker(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_ticker, EXCHANGE_NAME, market_type, symbol, MessageType::Ticker)\n}\n\n#[test_case(MarketType::Spot)]\n#[test_case(MarketType::LinearSwap)]\n#[tokio::test(flavor = \"multi_thread\", worker_threads = 8)]\nasync fn test_crawl_candlestick(market_type: MarketType) {\n    gen_test_crawl_candlestick!(EXCHANGE_NAME, market_type)\n}\n"
  },
  {
    "path": "crypto-crawler/tests/zbg.rs",
    "content": "#[macro_use]\nmod utils;\n\nuse test_case::test_case;\n\nuse crypto_crawler::*;\nuse crypto_market_type::MarketType;\nuse crypto_msg_type::MessageType;\nuse utils::parse;\n\nconst EXCHANGE_NAME: &str = \"zbg\";\n\n#[test_case(MarketType::Spot)]\n#[tokio::test(flavor = \"multi_thread\", worker_threads = 8)]\nasync fn test_crawl_trade_all(market_type: MarketType) {\n    test_all_symbols!(crawl_trade, EXCHANGE_NAME, market_type, MessageType::Trade)\n}\n\n#[test_case(MarketType::Spot, \"btc_usdt\")]\n// #[test_case(MarketType::InverseSwap, \"BTC_USD-R\")]\n// #[test_case(MarketType::LinearSwap, \"BTC_USDT\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_trade(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_trade, EXCHANGE_NAME, market_type, symbol, MessageType::Trade)\n}\n\n#[test_case(MarketType::Spot, \"btc_usdt\")]\n// #[test_case(MarketType::InverseSwap, \"BTC_USD-R\")]\n// #[test_case(MarketType::LinearSwap, \"BTC_USDT\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_l2_event(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_l2_event, EXCHANGE_NAME, market_type, symbol, MessageType::L2Event)\n}\n\n#[test_case(MarketType::Spot, \"btc_usdt\")]\n// #[test_case(MarketType::InverseSwap, \"BTC_USD-R\")]\n// #[test_case(MarketType::LinearSwap, \"BTC_USDT\")]\nfn test_crawl_l2_snapshot(market_type: MarketType, symbol: &str) {\n    test_crawl_restful!(\n        crawl_l2_snapshot,\n        EXCHANGE_NAME,\n        market_type,\n        symbol,\n        MessageType::L2Snapshot\n    )\n}\n\n#[test_case(MarketType::Spot)]\n#[test_case(MarketType::InverseSwap)]\n#[test_case(MarketType::LinearSwap)]\nfn test_crawl_l2_snapshot_without_symbol(market_type: MarketType) {\n    test_crawl_restful_all_symbols!(\n        crawl_l2_snapshot,\n        EXCHANGE_NAME,\n        market_type,\n        MessageType::L2Snapshot\n    )\n}\n\n#[test_case(MarketType::Spot, \"btc_usdt\")]\n// #[test_case(MarketType::InverseSwap, \"BTC_USD-R\")]\n// #[test_case(MarketType::LinearSwap, \"BTC_USDT\")]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn test_crawl_ticker(market_type: MarketType, symbol: &str) {\n    test_one_symbol!(crawl_ticker, EXCHANGE_NAME, market_type, symbol, MessageType::Ticker)\n}\n\n#[test_case(MarketType::Spot)]\n// #[test_case(MarketType::InverseSwap)]\n// #[test_case(MarketType::LinearSwap)]\n#[tokio::test(flavor = \"multi_thread\", worker_threads = 8)]\nasync fn test_crawl_candlestick(market_type: MarketType) {\n    gen_test_crawl_candlestick!(EXCHANGE_NAME, market_type)\n}\n"
  },
  {
    "path": "crypto-market-type/Cargo.toml",
    "content": "[package]\nname = \"crypto-market-type\"\nversion = \"1.1.5\"\nauthors = [\"soulmachine <soulmachine@gmail.com>\"]\nedition = \"2021\"\ndescription   = \"Cryptocurrenty market type\"\nlicense = \"Apache-2.0\"\nrepository = \"https://github.com/crypto-crawler/crypto-crawler-rs/tree/main/crypto-market-type\"\nkeywords = [\"cryptocurrency\", \"blockchain\", \"trading\"]\n\n[dependencies]\nserde = { version = \"1\", features = [\"derive\"] }\nstrum = \"0.24\"\nstrum_macros = \"0.24\"\n"
  },
  {
    "path": "crypto-market-type/include/crypto_market_type.h",
    "content": "/* Licensed under Apache-2.0 */\n#ifndef CRYPTO_MARKET_TYPE_H_\n#define CRYPTO_MARKET_TYPE_H_\n\n/**\n * Market type.\n *\n * * In spot market, cryptocurrencies are traded for immediate delivery, see\n * https://en.wikipedia.org/wiki/Spot_market.\n * * In futures market, delivery is set at a specified time in the future, see\n * https://en.wikipedia.org/wiki/Futures_exchange.\n * * Swap market is a variant of futures market with no expiry date.\n *\n * ## Margin\n *\n * A market can have margin enabled or disabled.\n *\n * * All contract markets are margin enabled, including future, swap and option.\n * * Most spot markets don't have margin enabled, only a few exchanges have spot\n * market with margin enabled.\n *\n * ## Linear VS. Inverse\n *\n * A market can be inverse or linear.\n * * Linear means USDT-margined, i.e., you can use USDT as collateral\n * * Inverse means coin-margined, i.e., you can use BTC as collateral.\n * * Spot market is always linear.\n *\n * **Margin and Inverse are orthogonal.**\n */\ntypedef enum {\n  Unknown,\n  Spot,\n  LinearFuture,\n  InverseFuture,\n  LinearSwap,\n  InverseSwap,\n  AmericanOption,\n  EuropeanOption,\n  QuantoFuture,\n  QuantoSwap,\n  Move,\n  BVOL,\n} MarketType;\n\n#endif /* CRYPTO_MARKET_TYPE_H_ */\n"
  },
  {
    "path": "crypto-market-type/src/lib.rs",
    "content": "use serde::{Deserialize, Serialize};\nuse strum_macros::{Display, EnumString};\n\n/// Market type.\n///\n/// * In spot market, cryptocurrencies are traded for immediate delivery, see https://en.wikipedia.org/wiki/Spot_market.\n/// * In futures market, delivery is set at a specified time in the future, see https://en.wikipedia.org/wiki/Futures_exchange.\n/// * Swap market is a variant of futures market with no expiry date.\n///\n/// ## Margin\n///\n/// A market can have margin enabled or disabled.\n///\n/// * All contract markets are margin enabled, including future, swap and\n///   option.\n/// * Most spot markets don't have margin enabled, only a few exchanges have\n///   spot market with margin enabled.\n///\n/// ## Linear VS. Inverse\n///\n/// A market can be inverse or linear.\n\n/// * Linear means USDT-margined, i.e., you can use USDT as collateral\n/// * Inverse means coin-margined, i.e., you can use BTC as collateral.\n/// * Spot market is always linear.\n///\n/// **Margin and Inverse are orthogonal.**\n#[repr(C)]\n#[derive(Copy, Clone, Serialize, Deserialize, Display, Debug, EnumString, PartialEq, Hash, Eq)]\n#[serde(rename_all = \"snake_case\")]\n#[strum(serialize_all = \"snake_case\")]\npub enum MarketType {\n    Unknown,\n    Spot,\n    LinearFuture,\n    InverseFuture,\n    LinearSwap,\n    InverseSwap,\n\n    AmericanOption,\n    EuropeanOption,\n\n    QuantoFuture,\n    QuantoSwap,\n\n    Move,\n    #[serde(rename = \"bvol\")]\n    #[allow(clippy::upper_case_acronyms)]\n    BVOL,\n}\n\n/// Get market types of a cryptocurrency exchange.\npub fn get_market_types(exchange: &str) -> Vec<MarketType> {\n    match exchange {\n        \"binance\" => vec![\n            MarketType::Spot,\n            MarketType::LinearFuture,\n            MarketType::InverseFuture,\n            MarketType::LinearSwap,\n            MarketType::InverseSwap,\n            // MarketType::EuropeanOption, // binance has shutdown option markets.\n        ],\n        \"bitfinex\" => vec![MarketType::Spot, MarketType::LinearSwap],\n        \"bitget\" => vec![\n            MarketType::Spot,\n            MarketType::InverseSwap, /* TODO: Bitget's coin-margined swap market is a kind of\n                                      * mixed contract */\n            MarketType::LinearSwap,\n            MarketType::InverseFuture,\n        ],\n        \"bithumb\" => vec![MarketType::Spot],\n        // BitMEX only handles Bitcoin. All profit and loss is in Bitcoin\n        \"bitmex\" => vec![\n            MarketType::Spot,\n            MarketType::LinearSwap,\n            MarketType::InverseSwap,\n            MarketType::QuantoSwap,\n            MarketType::LinearFuture,\n            MarketType::InverseFuture,\n            MarketType::QuantoFuture,\n        ],\n        \"bitstamp\" => vec![MarketType::Spot],\n        \"bitz\" => vec![MarketType::Spot, MarketType::InverseSwap, MarketType::LinearSwap],\n        \"bybit\" => vec![MarketType::InverseSwap, MarketType::LinearSwap, MarketType::InverseFuture],\n        \"coinbase_pro\" => vec![MarketType::Spot],\n        // Deribit only accepts Bitcoin as funds to deposit.\n        \"deribit\" => vec![\n            MarketType::InverseFuture,\n            MarketType::InverseSwap,\n            MarketType::EuropeanOption, // inverse\n        ],\n        \"dydx\" => vec![MarketType::LinearSwap],\n        \"ftx\" => vec![\n            MarketType::Spot,\n            MarketType::LinearFuture,\n            MarketType::LinearSwap,\n            MarketType::Move,\n            MarketType::BVOL,\n        ],\n        \"gate\" => vec![\n            MarketType::Spot,\n            MarketType::InverseFuture,\n            MarketType::LinearFuture,\n            MarketType::InverseSwap,\n            MarketType::LinearSwap,\n        ],\n        \"huobi\" => vec![\n            MarketType::Spot,\n            MarketType::InverseFuture,\n            MarketType::LinearSwap,\n            MarketType::InverseSwap,\n            // MarketType::EuropeanOption,\n        ],\n        \"kraken\" => vec![MarketType::Spot, MarketType::InverseFuture, MarketType::InverseSwap],\n        \"kucoin\" => vec![\n            MarketType::Spot,\n            MarketType::LinearSwap,\n            MarketType::InverseSwap,\n            MarketType::InverseFuture,\n        ],\n        \"mxc\" | \"mexc\" => vec![MarketType::Spot, MarketType::LinearSwap, MarketType::InverseSwap],\n        \"okex\" | \"okx\" => vec![\n            MarketType::Spot,\n            MarketType::LinearFuture,\n            MarketType::InverseFuture,\n            MarketType::LinearSwap,\n            MarketType::InverseSwap,\n            MarketType::EuropeanOption,\n        ],\n        \"zb\" => vec![MarketType::Spot, MarketType::LinearSwap],\n        \"zbg\" => vec![MarketType::Spot, MarketType::InverseSwap, MarketType::LinearSwap],\n        _ => panic!(\"Unknown exchange {exchange}\"),\n    }\n}\n"
  },
  {
    "path": "crypto-markets/Cargo.toml",
    "content": "[package]\nname = \"crypto-markets\"\nversion = \"1.3.11\"\nauthors = [\"soulmachine <soulmachine@gmail.com>\"]\nedition = \"2021\"\ndescription   = \"Fetch trading markets from a cryptocurrency exchange\"\nlicense = \"Apache-2.0\"\nrepository = \"https://github.com/crypto-crawler/crypto-crawler-rs/tree/main/crypto-markets\"\nkeywords = [\"cryptocurrency\", \"blockchain\", \"trading\"]\n\n[dependencies]\nchrono = \"0.4.24\"\ncrypto-market-type = \"1.1.5\"\ncrypto-pair = \"2.3.13\"\nreqwest = { version = \"0.11.14\", features = [\"blocking\", \"gzip\", \"socks\"] }\nserde = { version = \"1.0.157\", features = [\"derive\"] }\nserde_json = \"1.0.94\"\n\n[dev_dependencies]\ncrypto-contract-value = \"1.7.13\"\ntest-case = \"1\"\n"
  },
  {
    "path": "crypto-markets/README.md",
    "content": "# crypto-markets\n\n[![](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)\n[![](https://img.shields.io/crates/v/crypto-markets.svg)](https://crates.io/crates/crypto-markets)\n[![](https://docs.rs/crypto-markets/badge.svg)](https://docs.rs/crypto-markets)\n==========\n\nFetch trading markets from a cryptocurrency exchange.\n\n## Example\n\n```rust\nuse crypto_markets::{fetch_markets, MarketType};\n\nfn main() {\n    let markets = fetch_markets(\"Binance\", MarketType::Spot).unwrap();\n    println!(\"{}\", serde_json::to_string_pretty(&markets).unwrap())\n}\n```\n"
  },
  {
    "path": "crypto-markets/src/error.rs",
    "content": "use std::{error::Error as StdError, fmt};\n\npub(crate) type Result<T> = std::result::Result<T, Error>;\n\n#[derive(Debug)]\npub struct Error(pub String);\n\nimpl fmt::Display for Error {\n    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {\n        write!(f, \"{}\", self.0)\n    }\n}\n\nimpl StdError for Error {}\n\nimpl From<reqwest::Error> for Error {\n    fn from(err: reqwest::Error) -> Self {\n        Error(err.to_string())\n    }\n}\n\nimpl From<serde_json::Error> for Error {\n    fn from(err: serde_json::Error) -> Self {\n        Error(err.to_string())\n    }\n}\n"
  },
  {
    "path": "crypto-markets/src/exchanges/binance/binance_inverse.rs",
    "content": "use super::utils::{binance_http_get, parse_filter};\nuse crate::{error::Result, market::*, Market, MarketType};\n\nuse serde::{Deserialize, Serialize};\nuse serde_json::Value;\nuse std::collections::HashMap;\n\n#[derive(Serialize, Deserialize)]\nstruct BinanceResponse<T: Sized> {\n    symbols: Vec<T>,\n}\n\n#[derive(Serialize, Deserialize)]\n#[allow(non_snake_case)]\nstruct FutureMarket {\n    symbol: String,\n    pair: String,\n    contractType: String,\n    deliveryDate: u64,\n    onboardDate: u64,\n    contractStatus: String,\n    contractSize: f64,\n    marginAsset: String,\n    maintMarginPercent: String,\n    requiredMarginPercent: String,\n    baseAsset: String,\n    quoteAsset: String,\n    pricePrecision: i64,\n    quantityPrecision: i64,\n    baseAssetPrecision: i64,\n    quotePrecision: i64,\n    equalQtyPrecision: i64,\n    triggerProtect: String,\n    underlyingType: String,\n    filters: Vec<HashMap<String, Value>>,\n    orderTypes: Vec<String>,\n    timeInForce: Vec<String>,\n    #[serde(flatten)]\n    extra: HashMap<String, Value>,\n}\n\n// see <https://binance-docs.github.io/apidocs/delivery/en/#exchange-information>\nfn fetch_inverse_markets_raw() -> Result<Vec<FutureMarket>> {\n    let txt = binance_http_get(\"https://dapi.binance.com/dapi/v1/exchangeInfo\")?;\n    let resp = serde_json::from_str::<BinanceResponse<FutureMarket>>(&txt)?;\n    let symbols: Vec<FutureMarket> =\n        resp.symbols.into_iter().filter(|m| m.contractStatus == \"TRADING\").collect();\n    Ok(symbols)\n}\n\npub(super) fn fetch_inverse_future_symbols() -> Result<Vec<String>> {\n    let symbols = fetch_inverse_markets_raw()?\n        .into_iter()\n        .filter(|m| m.contractType != \"PERPETUAL\")\n        .map(|m| m.symbol)\n        .collect::<Vec<String>>();\n    Ok(symbols)\n}\n\npub(super) fn fetch_inverse_swap_symbols() -> Result<Vec<String>> {\n    let symbols = fetch_inverse_markets_raw()?\n        .into_iter()\n        .filter(|m| m.contractType == \"PERPETUAL\")\n        .map(|m| m.symbol)\n        .collect::<Vec<String>>();\n    Ok(symbols)\n}\n\nfn fetch_future_markets_internal() -> Result<Vec<Market>> {\n    let raw_markets = fetch_inverse_markets_raw()?;\n    let markets = raw_markets\n        .into_iter()\n        .map(|m| {\n            Market {\n                exchange: \"binance\".to_string(),\n                market_type: if m.contractType == \"PERPETUAL\" {\n                    MarketType::InverseSwap\n                } else {\n                    MarketType::InverseFuture\n                },\n                symbol: m.symbol.clone(),\n                base_id: m.baseAsset.clone(),\n                quote_id: m.quoteAsset.clone(),\n                settle_id: Some(m.marginAsset.clone()),\n                base: m.baseAsset.clone(),\n                quote: m.quoteAsset.clone(),\n                settle: Some(m.marginAsset.clone()),\n                active: m.contractStatus == \"TRADING\",\n                margin: true,\n                // see https://www.binance.com/en/fee/futureFee\n                fees: Fees { maker: 0.00015, taker: 0.0004 },\n                precision: Precision {\n                    tick_size: 1.0 / (10_i64.pow(m.pricePrecision as u32) as f64),\n                    lot_size: 1.0 / (10_i64.pow(m.quantityPrecision as u32) as f64),\n                },\n                quantity_limit: Some(QuantityLimit {\n                    min: parse_filter(&m.filters, \"LOT_SIZE\", \"minQty\").parse::<f64>().ok(),\n                    max: Some(\n                        parse_filter(&m.filters, \"LOT_SIZE\", \"maxQty\").parse::<f64>().unwrap(),\n                    ),\n                    notional_min: None,\n                    notional_max: None,\n                }),\n                contract_value: Some(m.contractSize),\n                delivery_date: if m.contractType == \"PERPETUAL\" {\n                    None\n                } else {\n                    Some(m.deliveryDate)\n                },\n                info: serde_json::to_value(&m).unwrap().as_object().unwrap().clone(),\n            }\n        })\n        .collect::<Vec<Market>>();\n    Ok(markets)\n}\n\npub(super) fn fetch_inverse_future_markets() -> Result<Vec<Market>> {\n    let markets = fetch_future_markets_internal()?\n        .into_iter()\n        .filter(|m| m.market_type == MarketType::InverseFuture)\n        .collect();\n    Ok(markets)\n}\n\npub(super) fn fetch_inverse_swap_markets() -> Result<Vec<Market>> {\n    let markets = fetch_future_markets_internal()?\n        .into_iter()\n        .filter(|m| m.market_type == MarketType::InverseSwap)\n        .collect();\n    Ok(markets)\n}\n"
  },
  {
    "path": "crypto-markets/src/exchanges/binance/binance_linear.rs",
    "content": "use super::utils::{binance_http_get, parse_filter};\nuse crate::{error::Result, market::*, Market, MarketType};\n\nuse serde::{Deserialize, Serialize};\nuse serde_json::Value;\nuse std::collections::HashMap;\n\n#[derive(Serialize, Deserialize)]\nstruct BinanceResponse<T: Sized> {\n    symbols: Vec<T>,\n}\n\n#[derive(Serialize, Deserialize)]\n#[allow(non_snake_case)]\nstruct LinearSwapMarket {\n    symbol: String,\n    pair: String,\n    contractType: String,\n    deliveryDate: u64,\n    onboardDate: u64,\n    status: String,\n    maintMarginPercent: String,\n    requiredMarginPercent: String,\n    baseAsset: String,\n    quoteAsset: String,\n    marginAsset: String,\n    pricePrecision: i64,\n    quantityPrecision: i64,\n    baseAssetPrecision: i64,\n    quotePrecision: i64,\n    underlyingType: String,\n    triggerProtect: String,\n    filters: Vec<HashMap<String, Value>>,\n    orderTypes: Vec<String>,\n    timeInForce: Vec<String>,\n    #[serde(flatten)]\n    extra: HashMap<String, Value>,\n}\n\n// see <https://binance-docs.github.io/apidocs/futures/en/#exchange-information>\nfn fetch_linear_markets_raw() -> Result<Vec<LinearSwapMarket>> {\n    let txt = binance_http_get(\"https://fapi.binance.com/fapi/v1/exchangeInfo\")?;\n    let resp = serde_json::from_str::<BinanceResponse<LinearSwapMarket>>(&txt)?;\n    let symbols: Vec<LinearSwapMarket> =\n        resp.symbols.into_iter().filter(|m| m.status == \"TRADING\").collect();\n    Ok(symbols)\n}\n\npub(super) fn fetch_linear_swap_symbols() -> Result<Vec<String>> {\n    let symbols = fetch_linear_markets_raw()?\n        .into_iter()\n        .filter(|m| m.contractType == \"PERPETUAL\")\n        .map(|m| m.symbol)\n        .collect::<Vec<String>>();\n    Ok(symbols)\n}\n\npub(super) fn fetch_linear_future_symbols() -> Result<Vec<String>> {\n    let symbols = fetch_linear_markets_raw()?\n        .into_iter()\n        .filter(|m| m.contractType != \"PERPETUAL\")\n        .map(|m| m.symbol)\n        .collect::<Vec<String>>();\n    Ok(symbols)\n}\n\nfn fetch_linear_markets() -> Result<Vec<Market>> {\n    let raw_markets = fetch_linear_markets_raw()?;\n    let markets = raw_markets\n        .into_iter()\n        .map(|m| {\n            Market {\n                exchange: \"binance\".to_string(),\n                market_type: if m.contractType == \"PERPETUAL\" {\n                    MarketType::LinearSwap\n                } else {\n                    MarketType::LinearFuture\n                },\n                symbol: m.symbol.clone(),\n                base_id: m.baseAsset.clone(),\n                quote_id: m.quoteAsset.clone(),\n                settle_id: Some(m.marginAsset.clone()),\n                base: m.baseAsset.clone(),\n                quote: m.quoteAsset.clone(),\n                settle: Some(m.marginAsset.clone()),\n                active: m.status == \"TRADING\",\n                margin: true,\n                // see https://www.binance.com/en/fee/futureFee\n                fees: Fees { maker: 0.0002, taker: 0.0004 },\n                precision: Precision {\n                    tick_size: 1.0 / (10_i64.pow(m.pricePrecision as u32) as f64),\n                    lot_size: 1.0 / (10_i64.pow(m.quantityPrecision as u32) as f64),\n                },\n                quantity_limit: Some(QuantityLimit {\n                    min: parse_filter(&m.filters, \"LOT_SIZE\", \"minQty\").parse::<f64>().ok(),\n                    max: Some(\n                        parse_filter(&m.filters, \"LOT_SIZE\", \"maxQty\").parse::<f64>().unwrap(),\n                    ),\n                    notional_min: None,\n                    notional_max: None,\n                }),\n                contract_value: Some(1.0),\n                delivery_date: if m.contractType == \"PERPETUAL\" {\n                    None\n                } else {\n                    Some(m.deliveryDate)\n                },\n                info: serde_json::to_value(&m).unwrap().as_object().unwrap().clone(),\n            }\n        })\n        .collect::<Vec<Market>>();\n    Ok(markets)\n}\n\npub(super) fn fetch_linear_swap_markets() -> Result<Vec<Market>> {\n    let markets = fetch_linear_markets()?;\n    let swap_markets =\n        markets.into_iter().filter(|m| m.market_type == MarketType::LinearSwap).collect();\n    Ok(swap_markets)\n}\n\npub(super) fn fetch_linear_future_markets() -> Result<Vec<Market>> {\n    let markets = fetch_linear_markets()?;\n    let future_markets =\n        markets.into_iter().filter(|m| m.market_type == MarketType::LinearFuture).collect();\n    Ok(future_markets)\n}\n"
  },
  {
    "path": "crypto-markets/src/exchanges/binance/binance_option.rs",
    "content": "use super::utils::binance_http_get;\nuse crate::{error::Result, market::*, Market, MarketType};\n\nuse serde::{Deserialize, Serialize};\nuse serde_json::Value;\nuse std::collections::HashMap;\n#[derive(Serialize, Deserialize)]\nstruct BinanceResponse<T: Sized> {\n    symbols: Vec<T>,\n}\n\n#[derive(Serialize, Deserialize)]\n#[allow(non_snake_case)]\nstruct OptionMarket {\n    id: i64,\n    contractId: i64,\n    underlying: String,\n    quoteAsset: String,\n    symbol: String,\n    unit: String,\n    minQty: String,\n    maxQty: String,\n    priceScale: i64,\n    quantityScale: i64,\n    side: String,\n    makerFeeRate: String,\n    takerFeeRate: String,\n    expiryDate: u64,\n    #[serde(flatten)]\n    extra: HashMap<String, Value>,\n}\n\nfn fetch_option_markets_raw() -> Result<Vec<OptionMarket>> {\n    #[derive(Serialize, Deserialize)]\n    #[allow(non_snake_case)]\n    struct OptionData {\n        timezone: String,\n        serverTime: i64,\n        optionContracts: Vec<Value>,\n        optionAssets: Vec<Value>,\n        optionSymbols: Vec<OptionMarket>,\n    }\n    #[derive(Serialize, Deserialize)]\n    #[allow(non_snake_case)]\n    struct BinanceOptionResponse {\n        code: i64,\n        msg: String,\n        data: OptionData,\n    }\n\n    let txt = binance_http_get(\"https://vapi.binance.com/vapi/v1/exchangeInfo\")?;\n    let resp = serde_json::from_str::<BinanceOptionResponse>(&txt)?;\n    Ok(resp.data.optionSymbols)\n}\n\npub(super) fn fetch_option_symbols() -> Result<Vec<String>> {\n    let symbols =\n        fetch_option_markets_raw()?.into_iter().map(|m| m.symbol).collect::<Vec<String>>();\n    Ok(symbols)\n}\n\npub(super) fn fetch_option_markets() -> Result<Vec<Market>> {\n    let raw_markets = fetch_option_markets_raw()?;\n    let markets = raw_markets\n        .into_iter()\n        .map(|m| {\n            let base_currency = m.underlying.strip_suffix(m.quoteAsset.as_str()).unwrap();\n            Market {\n                exchange: \"binance\".to_string(),\n                market_type: MarketType::EuropeanOption,\n                symbol: m.symbol.clone(),\n                base_id: base_currency.to_string(),\n                quote_id: m.quoteAsset.clone(),\n                settle_id: Some(m.quoteAsset.clone()),\n                base: base_currency.to_string(),\n                quote: m.quoteAsset.clone(),\n                settle: Some(m.quoteAsset.clone()),\n                active: true,\n                margin: true,\n                // see https://www.binance.com/en/fee/optionFee\n                fees: Fees {\n                    maker: m.makerFeeRate.parse::<f64>().unwrap(),\n                    taker: m.takerFeeRate.parse::<f64>().unwrap(),\n                },\n                precision: Precision {\n                    tick_size: 1.0 / (10_i64.pow(m.priceScale as u32) as f64),\n                    lot_size: 1.0 / (10_i64.pow(m.quantityScale as u32) as f64),\n                },\n                quantity_limit: Some(QuantityLimit {\n                    min: m.minQty.parse::<f64>().ok(),\n                    max: Some(m.maxQty.parse::<f64>().unwrap()),\n                    notional_min: None,\n                    notional_max: None,\n                }),\n                contract_value: Some(1.0),\n                delivery_date: Some(m.expiryDate),\n                info: serde_json::to_value(&m).unwrap().as_object().unwrap().clone(),\n            }\n        })\n        .collect::<Vec<Market>>();\n    Ok(markets)\n}\n"
  },
  {
    "path": "crypto-markets/src/exchanges/binance/binance_spot.rs",
    "content": "use super::utils::{binance_http_get, parse_filter};\nuse crate::{error::Result, market::*, Market, MarketType};\n\nuse serde::{Deserialize, Serialize};\nuse serde_json::Value;\nuse std::collections::HashMap;\n\n#[derive(Serialize, Deserialize)]\nstruct BinanceResponse<T: Sized> {\n    symbols: Vec<T>,\n}\n\n#[derive(Serialize, Deserialize)]\n#[allow(non_snake_case)]\nstruct SpotMarket {\n    symbol: String,\n    status: String,\n    baseAsset: String,\n    baseAssetPrecision: i32,\n    quoteAsset: String,\n    quotePrecision: i32,\n    quoteAssetPrecision: i32,\n    isSpotTradingAllowed: bool,\n    isMarginTradingAllowed: bool,\n    filters: Vec<HashMap<String, Value>>,\n    #[serde(flatten)]\n    extra: HashMap<String, Value>,\n}\n\n// see <https://binance-docs.github.io/apidocs/spot/en/#exchange-information>\nfn fetch_spot_markets_raw() -> Result<Vec<SpotMarket>> {\n    let txt = binance_http_get(\"https://api.binance.com/api/v3/exchangeInfo\")?;\n    let resp = serde_json::from_str::<BinanceResponse<SpotMarket>>(&txt)?;\n    Ok(resp.symbols.into_iter().filter(|s| s.symbol != \"123456\").collect())\n}\n\npub(super) fn fetch_spot_symbols() -> Result<Vec<String>> {\n    let symbols = fetch_spot_markets_raw()?\n        .into_iter()\n        .filter(|m| m.status == \"TRADING\" && m.isSpotTradingAllowed)\n        .map(|m| m.symbol)\n        .collect::<Vec<String>>();\n    Ok(symbols)\n}\n\npub(super) fn fetch_spot_markets() -> Result<Vec<Market>> {\n    let raw_markets = fetch_spot_markets_raw()?;\n    let markets = raw_markets\n        .into_iter()\n        .map(|m| {\n            Market {\n                exchange: \"binance\".to_string(),\n                market_type: MarketType::Spot,\n                symbol: m.symbol.clone(),\n                base_id: m.baseAsset.clone(),\n                quote_id: m.quoteAsset.clone(),\n                settle_id: None,\n                base: m.baseAsset.clone(),\n                quote: m.quoteAsset.clone(),\n                settle: None,\n                active: m.status == \"TRADING\" && m.isSpotTradingAllowed,\n                margin: m.isMarginTradingAllowed,\n                // see https://www.binance.com/en/fee/trading\n                fees: Fees { maker: 0.001, taker: 0.001 },\n                precision: Precision {\n                    tick_size: parse_filter(&m.filters, \"PRICE_FILTER\", \"tickSize\")\n                        .parse::<f64>()\n                        .unwrap(),\n                    lot_size: parse_filter(&m.filters, \"LOT_SIZE\", \"stepSize\")\n                        .parse::<f64>()\n                        .unwrap(),\n                },\n                quantity_limit: Some(QuantityLimit {\n                    min: parse_filter(&m.filters, \"LOT_SIZE\", \"minQty\").parse::<f64>().ok(),\n                    max: Some(\n                        parse_filter(&m.filters, \"LOT_SIZE\", \"maxQty\").parse::<f64>().unwrap(),\n                    ),\n                    notional_min: None,\n                    notional_max: None,\n                }),\n                contract_value: None,\n                delivery_date: None,\n                info: serde_json::to_value(&m).unwrap().as_object().unwrap().clone(),\n            }\n        })\n        .collect::<Vec<Market>>();\n    Ok(markets)\n}\n"
  },
  {
    "path": "crypto-markets/src/exchanges/binance/mod.rs",
    "content": "pub(super) mod binance_inverse;\npub(super) mod binance_linear;\npub(super) mod binance_option;\npub(super) mod binance_spot;\nmod utils;\n\nuse crate::{error::Result, Market, MarketType};\n\npub(crate) fn fetch_symbols(market_type: MarketType) -> Result<Vec<String>> {\n    match market_type {\n        MarketType::Spot => binance_spot::fetch_spot_symbols(),\n        MarketType::LinearFuture => binance_linear::fetch_linear_future_symbols(),\n        MarketType::InverseFuture => binance_inverse::fetch_inverse_future_symbols(),\n        MarketType::LinearSwap => binance_linear::fetch_linear_swap_symbols(),\n        MarketType::InverseSwap => binance_inverse::fetch_inverse_swap_symbols(),\n        MarketType::EuropeanOption => binance_option::fetch_option_symbols(),\n        _ => panic!(\"Unsupported market_type: {market_type}\"),\n    }\n}\n\npub(crate) fn fetch_markets(market_type: MarketType) -> Result<Vec<Market>> {\n    match market_type {\n        MarketType::Spot => binance_spot::fetch_spot_markets(),\n        MarketType::LinearFuture => binance_linear::fetch_linear_future_markets(),\n        MarketType::InverseFuture => binance_inverse::fetch_inverse_future_markets(),\n        MarketType::LinearSwap => binance_linear::fetch_linear_swap_markets(),\n        MarketType::InverseSwap => binance_inverse::fetch_inverse_swap_markets(),\n        MarketType::EuropeanOption => binance_option::fetch_option_markets(),\n        _ => panic!(\"Unsupported market_type: {market_type}\"),\n    }\n}\n"
  },
  {
    "path": "crypto-markets/src/exchanges/binance/utils.rs",
    "content": "use super::super::utils::http_get;\nuse crate::error::{Error, Result};\n\nuse serde_json::Value;\nuse std::collections::HashMap;\n\nfn check_code_in_body(resp: String) -> Result<String> {\n    let obj = serde_json::from_str::<HashMap<String, Value>>(&resp);\n    if obj.is_err() {\n        return Ok(resp);\n    }\n\n    match obj.unwrap().get(\"code\") {\n        Some(code) => {\n            if code.as_i64().unwrap() != 0 {\n                Err(Error(resp))\n            } else {\n                Ok(resp)\n            }\n        }\n        None => Ok(resp),\n    }\n}\n\npub(super) fn binance_http_get(url: &str) -> Result<String> {\n    let ret = http_get(url, None);\n    match ret {\n        Ok(resp) => check_code_in_body(resp),\n        Err(_) => ret,\n    }\n}\n\npub(super) fn parse_filter<'a>(\n    filters: &'a [HashMap<String, Value>],\n    filter_type: &'a str,\n    field: &'static str,\n) -> &'a str {\n    filters.iter().find(|x| x[\"filterType\"] == filter_type).unwrap()[field].as_str().unwrap()\n}\n"
  },
  {
    "path": "crypto-markets/src/exchanges/bitfinex.rs",
    "content": "use std::collections::HashMap;\n\nuse super::utils::http_get;\nuse crate::{\n    error::Result,\n    market::{Fees, Precision, QuantityLimit},\n    Market, MarketType,\n};\nuse serde::{Deserialize, Serialize};\nuse serde_json::Value;\n\npub(crate) fn fetch_symbols(market_type: MarketType) -> Result<Vec<String>> {\n    match market_type {\n        MarketType::Spot => fetch_spot_symbols(),\n        MarketType::LinearSwap => fetch_linear_swap_symbols(),\n        _ => panic!(\"Unsupported market_type: {market_type}\"),\n    }\n}\n\n#[derive(Serialize, Deserialize)]\nstruct RawMarket {\n    pair: String,\n    price_precision: i64,\n    initial_margin: String,\n    minimum_margin: String,\n    maximum_order_size: String,\n    minimum_order_size: String,\n    expiration: String,\n    margin: bool,\n    #[serde(flatten)]\n    extra: HashMap<String, Value>,\n}\n\nfn fetch_raw_markets() -> Result<Vec<RawMarket>> {\n    // can NOT use v2 API due to https://github.com/bitfinexcom/bitfinex-api-py/issues/95\n    let text = http_get(\"https://api.bitfinex.com/v1/symbols_details\", None)?;\n    let markets = serde_json::from_str::<Vec<RawMarket>>(&text)?;\n    let markets =\n        markets.into_iter().filter(|m| !m.pair.starts_with(\"test\")).collect::<Vec<RawMarket>>();\n    Ok(markets)\n}\n\npub(crate) fn fetch_markets(market_type: MarketType) -> Result<Vec<Market>> {\n    let raw_markets = fetch_raw_markets()?;\n    let raw_markets: Vec<RawMarket> = match market_type {\n        MarketType::Spot => raw_markets.into_iter().filter(|x| !x.pair.ends_with(\"f0\")).collect(),\n        MarketType::LinearSwap => {\n            raw_markets.into_iter().filter(|x| x.pair.ends_with(\"f0\")).collect()\n        }\n        _ => panic!(\"Unsupported market_type: {market_type}\"),\n    };\n    let markets: Vec<Market> = raw_markets\n        .into_iter()\n        .map(|m| {\n            let symbol = m.pair.to_uppercase();\n            let pair = crypto_pair::normalize_pair(&symbol, \"bitfinex\").unwrap();\n            let (base, quote) = {\n                let v: Vec<&str> = pair.split('/').collect();\n                (v[0].to_string(), v[1].to_string())\n            };\n            let (base_id, quote_id) = if symbol.contains(':') {\n                let v: Vec<&str> = symbol.split(':').collect();\n                (v[0].to_string(), v[1].to_string())\n            } else {\n                (symbol[..(symbol.len() - 3)].to_string(), symbol[(symbol.len() - 3)..].to_string())\n            };\n            Market {\n                exchange: \"bitfinex\".to_string(),\n                market_type,\n                symbol: format!(\"t{symbol}\"),\n                base_id,\n                quote_id: quote_id.clone(),\n                settle_id: if market_type == MarketType::LinearSwap {\n                    Some(quote_id)\n                } else {\n                    None\n                },\n                base,\n                quote: quote.clone(),\n                settle: if market_type == MarketType::LinearSwap { Some(quote) } else { None },\n                active: true,\n                margin: m.margin,\n                // see https://www.bitfinex.com/fees\n                fees: if market_type == MarketType::Spot {\n                    Fees { maker: 0.001, taker: 0.002 }\n                } else {\n                    Fees { maker: -0.0002, taker: 0.00075 }\n                },\n                precision: Precision {\n                    tick_size: 1.0 / (10_i64.pow(m.price_precision as u32) as f64),\n                    lot_size: 1.0 / (10_i64.pow(8_u32) as f64),\n                },\n                quantity_limit: Some(QuantityLimit {\n                    min: m.minimum_order_size.parse::<f64>().ok(),\n                    max: Some(m.maximum_order_size.parse::<f64>().unwrap()),\n                    notional_min: None,\n                    notional_max: None,\n                }),\n                contract_value: if market_type == MarketType::Spot { None } else { Some(1.0) },\n                delivery_date: None,\n                info: serde_json::to_value(&m).unwrap().as_object().unwrap().clone(),\n            }\n        })\n        .collect();\n    Ok(markets)\n}\n\n// see <https://docs.bitfinex.com/reference#rest-public-conf>\nfn fetch_spot_symbols() -> Result<Vec<String>> {\n    let text = http_get(\"https://api-pub.bitfinex.com/v2/conf/pub:list:pair:exchange\", None)?;\n    let pairs = serde_json::from_str::<Vec<Vec<String>>>(&text)?;\n    let symbols = pairs[0]\n        .iter()\n        .filter(|x| !x.starts_with(\"TEST\"))\n        .map(|p| format!(\"t{p}\"))\n        .collect::<Vec<String>>();\n    Ok(symbols)\n}\n\n// see <https://docs.bitfinex.com/reference#rest-public-conf>\nfn fetch_linear_swap_symbols() -> Result<Vec<String>> {\n    let text = http_get(\"https://api-pub.bitfinex.com/v2/conf/pub:list:pair:futures\", None)?;\n    let pairs = serde_json::from_str::<Vec<Vec<String>>>(&text)?;\n    let symbols = pairs[0]\n        .iter()\n        .filter(|x| !x.starts_with(\"TEST\"))\n        .map(|p| format!(\"t{p}\"))\n        .collect::<Vec<String>>();\n    Ok(symbols)\n}\n\n#[cfg(test)]\nmod tests {\n    use serde_json::Value;\n\n    use super::{\n        super::utils::http_get, fetch_linear_swap_symbols, fetch_raw_markets, fetch_spot_symbols,\n    };\n    use crate::error::Result;\n\n    fn _fetch_symbols(url: &str) -> Result<Vec<String>> {\n        let text = http_get(url, None)?;\n        let arr = serde_json::from_str::<Vec<Value>>(&text)?;\n        let arr = serde_json::from_value::<Vec<Value>>(arr[0].clone())?;\n        let symbols = arr\n            .iter()\n            .map(|p| format!(\"t{}\", p[0].as_str().unwrap()))\n            .filter(|x| !x.starts_with(\"tTEST\"))\n            .collect::<Vec<String>>();\n        Ok(symbols)\n    }\n\n    fn _fetch_spot_symbols() -> Result<Vec<String>> {\n        _fetch_symbols(\"https://api-pub.bitfinex.com/v2/conf/pub:info:pair\")\n    }\n\n    fn _fetch_linear_swap_symbols() -> Result<Vec<String>> {\n        _fetch_symbols(\"https://api-pub.bitfinex.com/v2/conf/pub:info:pair:futures\")\n    }\n\n    #[test]\n    fn test_spot_symbols() {\n        let mut symbols1 = _fetch_spot_symbols().unwrap();\n        let symbols2 = fetch_spot_symbols().unwrap();\n        assert_eq!(symbols1, symbols2);\n\n        let mut symbols3: Vec<String> = fetch_raw_markets()\n            .unwrap()\n            .into_iter()\n            .map(|m| format!(\"t{}\", m.pair.to_uppercase()))\n            .filter(|x| !x.ends_with(\"F0\"))\n            .collect();\n        symbols1.sort();\n        symbols3.sort();\n        // assert_eq!(symbols1, symbols3); // sometimes symbols3 has extra\n        // symbols that don't exist in symbols1\n    }\n\n    #[test]\n    fn test_linear_swap_symbols() {\n        let mut symbols1 = _fetch_linear_swap_symbols().unwrap();\n        let symbols2 = fetch_linear_swap_symbols().unwrap();\n        assert_eq!(symbols1, symbols2);\n\n        let mut symbols3: Vec<String> = fetch_raw_markets()\n            .unwrap()\n            .into_iter()\n            .map(|m| format!(\"t{}\", m.pair.to_uppercase()))\n            .filter(|x| x.ends_with(\"F0\"))\n            .collect();\n        symbols1.sort();\n        symbols3.sort();\n        assert_eq!(symbols1, symbols3);\n    }\n}\n"
  },
  {
    "path": "crypto-markets/src/exchanges/bitget/bitget_spot.rs",
    "content": "use std::collections::HashMap;\n\nuse super::{super::utils::http_get, EXCHANGE_NAME};\nuse crate::{\n    error::{Error, Result},\n    Fees, Market, Precision, QuantityLimit,\n};\n\nuse crypto_market_type::MarketType;\nuse serde::{Deserialize, Serialize};\nuse serde_json::Value;\n\n// See https://bitgetlimited.github.io/apidoc/en/spot/#get-all-instruments\n#[derive(Clone, Serialize, Deserialize)]\n#[allow(non_snake_case)]\nstruct SpotMarket {\n    symbol: String,         // symbol Id\n    symbolName: String,     // symbol name\n    baseCoin: String,       // Base coin\n    quoteCoin: String,      // Denomination coin\n    minTradeAmount: String, // Min trading amount\n    maxTradeAmount: String, // Max trading amount\n    takerFeeRate: String,   // Taker transaction fee rate\n    makerFeeRate: String,   // Maker transaction fee rate\n    priceScale: String,     // Maker transaction fee rate\n    quantityScale: String,  // Quantity scale\n    status: String,         // Status\n    #[serde(flatten)]\n    extra: HashMap<String, Value>,\n}\n\n#[derive(Serialize, Deserialize)]\n#[allow(non_snake_case)]\nstruct Response {\n    code: String,\n    msg: String,\n    data: Vec<SpotMarket>,\n    requestTime: i64,\n    #[serde(flatten)]\n    extra: HashMap<String, Value>,\n}\n\n// See https://bitgetlimited.github.io/apidoc/en/spot/#get-all-instruments\nfn fetch_spot_markets_raw() -> Result<Vec<SpotMarket>> {\n    let txt = http_get(\"https://api.bitget.com/api/spot/v1/public/products\", None)?;\n    let resp = serde_json::from_str::<Response>(&txt)?;\n    if resp.msg != \"success\" {\n        Err(Error(txt))\n    } else {\n        let markets = resp\n            .data\n            .into_iter()\n            // Ignored ETH_SPBL and BTC_SPBL for now because they're not tradable\n            .filter(|x| x.status == \"online\" && x.symbol.ends_with(\"USDT_SPBL\"))\n            .collect::<Vec<SpotMarket>>();\n        Ok(markets)\n    }\n}\n\npub(super) fn fetch_spot_symbols() -> Result<Vec<String>> {\n    let markets = fetch_spot_markets_raw()?;\n    let symbols: Vec<String> = markets.into_iter().map(|m| m.symbol).collect();\n    Ok(symbols)\n}\n\npub(super) fn fetch_spot_markets() -> Result<Vec<Market>> {\n    let markets: Vec<Market> = fetch_spot_markets_raw()?\n        .into_iter()\n        .map(|m| Market {\n            exchange: EXCHANGE_NAME.to_string(),\n            market_type: MarketType::Spot,\n            symbol: m.symbol.clone(),\n            base_id: m.baseCoin.clone(),\n            quote_id: m.quoteCoin.clone(),\n            settle_id: None,\n            base: m.baseCoin.clone(),\n            quote: m.quoteCoin.clone(),\n            settle: None,\n            active: m.status == \"online\",\n            margin: false,\n            fees: Fees {\n                maker: m.makerFeeRate.parse::<f64>().unwrap(),\n                taker: m.takerFeeRate.parse::<f64>().unwrap(),\n            },\n            precision: Precision {\n                tick_size: 1.0 / (10_i64.pow(m.priceScale.parse::<u32>().unwrap()) as f64),\n                lot_size: 1.0 / (10_i64.pow(m.quantityScale.parse::<u32>().unwrap()) as f64),\n            },\n            quantity_limit: Some(QuantityLimit {\n                min: m.minTradeAmount.parse::<f64>().ok(),\n                max: if m.maxTradeAmount.parse::<f64>().unwrap() > 0.0 {\n                    Some(m.maxTradeAmount.parse::<f64>().unwrap())\n                } else {\n                    None\n                },\n                notional_min: None,\n                notional_max: None,\n            }),\n            contract_value: None,\n            delivery_date: None,\n            info: serde_json::to_value(&m).unwrap().as_object().unwrap().clone(),\n        })\n        .collect();\n    Ok(markets)\n}\n"
  },
  {
    "path": "crypto-markets/src/exchanges/bitget/bitget_swap.rs",
    "content": "use std::collections::HashMap;\n\nuse super::{super::utils::http_get, EXCHANGE_NAME};\nuse crate::{\n    error::{Error, Result},\n    Fees, Market, Precision, QuantityLimit,\n};\n\nuse chrono::DateTime;\nuse crypto_market_type::MarketType;\nuse serde::{Deserialize, Serialize};\nuse serde_json::Value;\n\n// See https://bitgetlimited.github.io/apidoc/en/mix/#get-all-symbols\n#[derive(Clone, Serialize, Deserialize)]\n#[allow(non_snake_case)]\nstruct SwapMarket {\n    symbol: String,                  // symbol Id\n    baseCoin: String,                // Base coin\n    quoteCoin: String,               // Denomination coin\n    buyLimitPriceRatio: String,      // Buy price limit ratio 1%\n    sellLimitPriceRatio: String,     // Sell price limit ratio 1%\n    feeRateUpRatio: String,          // Rate of increase in handling fee%\n    takerFeeRate: String,            // Taker fee rate%\n    makerFeeRate: String,            // Market fee rate%\n    openCostUpRatio: String,         // Percentage of increase in opening cost%\n    supportMarginCoins: Vec<String>, // Support margin currency\n    minTradeNum: String,             // Minimum number of openings\n    priceEndStep: String,            // Price step\n    pricePlace: String,              // Price decimal places\n    volumePlace: String,             // Number of decimal places\n    #[serde(flatten)]\n    extra: HashMap<String, Value>,\n}\n\n#[derive(Serialize, Deserialize)]\n#[allow(non_snake_case)]\nstruct Response {\n    code: String,\n    msg: String,\n    data: Vec<SwapMarket>,\n    requestTime: i64,\n    #[serde(flatten)]\n    extra: HashMap<String, Value>,\n}\n\n// See https://bitgetlimited.github.io/apidoc/en/mix/#get-all-symbols\n// product_type: umcbl, LinearSwap; dmcbl, InverseSwap;\nfn fetch_swap_markets_raw(product_type: &str) -> Result<Vec<SwapMarket>> {\n    let txt = http_get(\n        format!(\"https://api.bitget.com/api/mix/v1/market/contracts?productType={product_type}\")\n            .as_str(),\n        None,\n    )?;\n    let resp = serde_json::from_str::<Response>(&txt)?;\n    if resp.msg != \"success\" { Err(Error(txt)) } else { Ok(resp.data) }\n}\n\npub(super) fn fetch_inverse_swap_symbols() -> Result<Vec<String>> {\n    let symbols = fetch_swap_markets_raw(\"dmcbl\")?\n        .into_iter()\n        .map(|m| m.symbol)\n        .filter(|symbol| symbol.ends_with(\"_DMCBL\"))\n        .collect::<Vec<String>>();\n    Ok(symbols)\n}\n\npub(super) fn fetch_inverse_future_symbols() -> Result<Vec<String>> {\n    let symbols = fetch_swap_markets_raw(\"dmcbl\")?\n        .into_iter()\n        .map(|m| m.symbol)\n        .filter(|symbol| !symbol.ends_with(\"_DMCBL\"))\n        .collect::<Vec<String>>();\n    Ok(symbols)\n}\n\npub(super) fn fetch_linear_swap_symbols() -> Result<Vec<String>> {\n    // see https://bitgetlimited.github.io/apidoc/en/mix/#producttype\n    let mut usdt_symbols =\n        fetch_swap_markets_raw(\"umcbl\")?.into_iter().map(|m| m.symbol).collect::<Vec<String>>();\n    let usdc_symbols =\n        fetch_swap_markets_raw(\"cmcbl\")?.into_iter().map(|m| m.symbol).collect::<Vec<String>>();\n    usdt_symbols.extend(usdc_symbols);\n    Ok(usdt_symbols)\n}\n\npub(super) fn fetch_inverse_swap_markets() -> Result<Vec<Market>> {\n    let markets = fetch_swap_markets_raw(\"dmcbl\")?\n        .into_iter()\n        .filter(|market| market.symbol.ends_with(\"_DMCBL\"))\n        .map(to_market)\n        .collect::<Vec<Market>>();\n    Ok(markets)\n}\n\npub(super) fn fetch_inverse_future_markets() -> Result<Vec<Market>> {\n    let markets = fetch_swap_markets_raw(\"dmcbl\")?\n        .into_iter()\n        .filter(|market| !market.symbol.ends_with(\"_DMCBL\"))\n        .map(to_market)\n        .collect::<Vec<Market>>();\n    Ok(markets)\n}\n\npub(super) fn fetch_linear_swap_markets() -> Result<Vec<Market>> {\n    let markets =\n        fetch_swap_markets_raw(\"umcbl\")?.into_iter().map(to_market).collect::<Vec<Market>>();\n    Ok(markets)\n}\n\nfn to_market(m: SwapMarket) -> Market {\n    let market_type = if m.symbol.ends_with(\"_UMCBL\") {\n        MarketType::LinearSwap\n    } else if m.symbol.ends_with(\"_DMCBL\") {\n        MarketType::InverseSwap\n    } else if m.symbol.contains(\"_UMCBL_\") {\n        MarketType::LinearFuture\n    } else if m.symbol.contains(\"_DMCBL_\") {\n        MarketType::InverseFuture\n    } else {\n        panic!(\"unexpected symbol: {}\", m.symbol);\n    };\n    let delivery_time = if market_type == MarketType::InverseFuture\n        || market_type == MarketType::LinearFuture\n    {\n        let date = m.symbol.split('_').last().unwrap();\n        debug_assert_eq!(date.len(), 6); // e.g., 230331\n        let year = &date[..2];\n        let month = &date[2..4];\n        let day = &date[4..];\n        let delivery_time =\n            DateTime::parse_from_rfc3339(format!(\"20{year}-{month}-{day}T00:00:00+00:00\").as_str())\n                .unwrap()\n                .timestamp_millis() as u64;\n        Some(delivery_time)\n    } else {\n        None\n    };\n    Market {\n        exchange: EXCHANGE_NAME.to_string(),\n        market_type,\n        symbol: m.symbol.clone(),\n        base_id: m.baseCoin.clone(),\n        quote_id: m.quoteCoin.clone(),\n        settle_id: Some(m.supportMarginCoins[0].clone()),\n        base: m.baseCoin.clone(),\n        quote: m.quoteCoin.clone(),\n        settle: Some(m.supportMarginCoins[0].clone()),\n        active: true,\n        margin: true,\n        fees: Fees {\n            maker: m.makerFeeRate.parse::<f64>().unwrap(),\n            taker: m.takerFeeRate.parse::<f64>().unwrap(),\n        },\n        precision: Precision {\n            tick_size: 1.0 / (10_i64.pow(m.pricePlace.parse::<u32>().unwrap()) as f64),\n            lot_size: 1.0 / (10_i64.pow(m.volumePlace.parse::<u32>().unwrap()) as f64),\n        },\n        quantity_limit: Some(QuantityLimit {\n            min: m.minTradeNum.parse::<f64>().ok(),\n            max: None,\n            notional_min: None,\n            notional_max: None,\n        }),\n        contract_value: Some(1.0), // TODO:\n        delivery_date: delivery_time,\n        info: serde_json::to_value(&m).unwrap().as_object().unwrap().clone(),\n    }\n}\n"
  },
  {
    "path": "crypto-markets/src/exchanges/bitget/mod.rs",
    "content": "pub(super) mod bitget_spot;\npub(super) mod bitget_swap;\n\nuse crate::{error::Result, Market, MarketType};\n\npub(super) const EXCHANGE_NAME: &str = \"bitget\";\n\npub(crate) fn fetch_symbols(market_type: MarketType) -> Result<Vec<String>> {\n    match market_type {\n        MarketType::Spot => bitget_spot::fetch_spot_symbols(),\n        MarketType::InverseSwap => bitget_swap::fetch_inverse_swap_symbols(),\n        MarketType::LinearSwap => bitget_swap::fetch_linear_swap_symbols(),\n        MarketType::InverseFuture => bitget_swap::fetch_inverse_future_symbols(),\n        _ => panic!(\"Unsupported market_type: {market_type}\"),\n    }\n}\n\npub(crate) fn fetch_markets(market_type: MarketType) -> Result<Vec<Market>> {\n    match market_type {\n        MarketType::Spot => bitget_spot::fetch_spot_markets(),\n        MarketType::InverseSwap => bitget_swap::fetch_inverse_swap_markets(),\n        MarketType::LinearSwap => bitget_swap::fetch_linear_swap_markets(),\n        MarketType::InverseFuture => bitget_swap::fetch_inverse_future_markets(),\n        _ => panic!(\"Unsupported market_type: {market_type}\"),\n    }\n}\n"
  },
  {
    "path": "crypto-markets/src/exchanges/bithumb.rs",
    "content": "use std::collections::HashMap;\n\nuse super::utils::http_get;\nuse crate::{\n    error::{Error, Result},\n    Fees, Market, MarketType, Precision,\n};\n\nuse serde::{Deserialize, Serialize};\nuse serde_json::Value;\n\npub(crate) fn fetch_symbols(market_type: MarketType) -> Result<Vec<String>> {\n    match market_type {\n        MarketType::Spot => fetch_spot_symbols(),\n        _ => panic!(\"Unsupported market_type: {market_type}\"),\n    }\n}\n\npub(crate) fn fetch_markets(market_type: MarketType) -> Result<Vec<Market>> {\n    match market_type {\n        MarketType::Spot => fetch_spot_markets(),\n        _ => panic!(\"Unsupported market_type: {market_type}\"),\n    }\n}\n\n#[derive(Serialize, Deserialize)]\n#[allow(non_snake_case)]\nstruct CoinConfig {\n    makerFeeRate: serde_json::Value, // String or i64\n    minTxAmt: Option<String>,\n    name: String,\n    depositStatus: String,\n    fullName: String,\n    takerFeeRate: serde_json::Value, // String or i64\n    minWithdraw: String,\n    withdrawFee: String,\n    withdrawStatus: String,\n    #[serde(flatten)]\n    extra: HashMap<String, Value>,\n}\n\n#[derive(Serialize, Deserialize)]\n#[allow(non_snake_case)]\nstruct PercentPrice {\n    multiplierDown: String,\n    multiplierUp: String,\n}\n\n#[derive(Serialize, Deserialize)]\n#[allow(non_snake_case)]\nstruct SpotConfig {\n    symbol: String,\n    percentPrice: PercentPrice,\n    accuracy: Vec<String>,\n    openPrice: String,\n    openTime: i64,\n    #[serde(flatten)]\n    extra: HashMap<String, Value>,\n}\n\n#[derive(Serialize, Deserialize)]\n#[allow(non_snake_case)]\nstruct Data {\n    coinConfig: Vec<CoinConfig>,\n    spotConfig: Vec<SpotConfig>,\n}\n\n#[derive(Serialize, Deserialize)]\nstruct Response {\n    data: Data,\n    code: String,\n    msg: String,\n    timestamp: i64,\n}\n\n// see https://github.com/bithumb-pro/bithumb.pro-official-api-docs/blob/master/rest-api.md#2-config-detail\nfn fetch_spot_coing() -> Result<Data> {\n    let txt = http_get(\"https://global-openapi.bithumb.pro/openapi/v1/spot/config\", None)?;\n    let resp = serde_json::from_str::<Response>(&txt)?;\n    if resp.code != \"0\" { Err(Error(txt)) } else { Ok(resp.data) }\n}\n\nfn fetch_spot_symbols() -> Result<Vec<String>> {\n    let symbols =\n        fetch_spot_coing()?.spotConfig.into_iter().map(|m| m.symbol).collect::<Vec<String>>();\n    Ok(symbols)\n}\n\nfn fetch_spot_markets() -> Result<Vec<Market>> {\n    let markets = fetch_spot_coing()?\n        .spotConfig\n        .into_iter()\n        .map(|m| {\n            let info = serde_json::to_value(&m).unwrap().as_object().unwrap().clone();\n            let pair = crypto_pair::normalize_pair(&m.symbol, \"bithumb\").unwrap();\n            let (base, quote) = {\n                let v: Vec<&str> = pair.split('/').collect();\n                (v[0].to_string(), v[1].to_string())\n            };\n            let (base_id, quote_id) = {\n                let v: Vec<&str> = m.symbol.split('-').collect();\n                (v[0].to_string(), v[1].to_string())\n            };\n            Market {\n                exchange: \"bithumb\".to_string(),\n                market_type: MarketType::Spot,\n                symbol: m.symbol,\n                base_id,\n                quote_id,\n                settle_id: None,\n                base,\n                quote,\n                settle: None,\n                active: true,\n                margin: false,\n                // see https://www.bitglobal.com/en-us/fee\n                fees: Fees { maker: 0.001, taker: 0.001 },\n                precision: Precision {\n                    tick_size: 1.0 / (10_i64.pow(m.accuracy[0].parse::<u32>().unwrap()) as f64),\n                    lot_size: 1.0 / (10_i64.pow(m.accuracy[1].parse::<u32>().unwrap()) as f64),\n                },\n                quantity_limit: None,\n                contract_value: None,\n                delivery_date: None,\n                info,\n            }\n        })\n        .collect::<Vec<Market>>();\n    Ok(markets)\n}\n"
  },
  {
    "path": "crypto-markets/src/exchanges/bitmex.rs",
    "content": "use std::collections::HashMap;\n\nuse super::utils::http_get;\nuse crate::{\n    error::Result,\n    market::{Fees, Precision},\n    Market, MarketType,\n};\n\nuse chrono::DateTime;\nuse crypto_pair::get_market_type;\nuse serde::{Deserialize, Serialize};\nuse serde_json::Value;\n\npub(crate) fn fetch_symbols(market_type: MarketType) -> Result<Vec<String>> {\n    let instruments = fetch_instruments(market_type)?;\n    Ok(instruments.into_iter().map(|x| x.symbol).collect::<Vec<String>>())\n}\n\npub(crate) fn fetch_markets(market_type: MarketType) -> Result<Vec<Market>> {\n    let instruments = fetch_instruments(market_type)?;\n    let markets: Vec<Market> = instruments\n        .into_iter()\n        .map(|x| {\n            let info = serde_json::to_value(&x).unwrap().as_object().unwrap().clone();\n            let base_id = x.underlying;\n            let quote_id = x.quoteCurrency;\n            let pair = crypto_pair::normalize_pair(&x.symbol, \"bitmex\").unwrap();\n            let (base, quote) = {\n                let v: Vec<&str> = pair.split('/').collect();\n                (v[0].to_string(), v[1].to_string())\n            };\n            let market_type = if market_type == MarketType::Unknown {\n                get_market_type(&x.symbol, \"bitmex\", None)\n            } else {\n                market_type\n            };\n\n            Market {\n                exchange: \"bitmex\".to_string(),\n                market_type,\n                symbol: x.symbol,\n                base_id,\n                quote_id,\n                settle_id: Some(x.settlCurrency.clone()),\n                base,\n                quote,\n                settle: Some(crypto_pair::normalize_currency(x.settlCurrency.as_str(), \"bitmex\")),\n                active: x.state == \"Open\",\n                margin: true,\n                fees: Fees { maker: x.makerFee, taker: x.takerFee },\n                precision: Precision { tick_size: x.tickSize, lot_size: x.lotSize },\n                quantity_limit: None,\n                contract_value: if market_type != MarketType::Spot {\n                    if let Some(y) = x.underlyingToSettleMultiplier {\n                        Some(x.multiplier / y)\n                    } else {\n                        Some(x.multiplier / x.quoteToSettleMultiplier.unwrap())\n                    }\n                } else {\n                    None\n                },\n                delivery_date: if let Some(expiry) = x.expiry {\n                    let timestamp = DateTime::parse_from_rfc3339(&expiry).unwrap();\n                    Some(timestamp.timestamp_millis() as u64)\n                } else {\n                    None\n                },\n                info,\n            }\n        })\n        .collect();\n    Ok(markets)\n}\n\n// https://bitmex.freshdesk.com/en/support/solutions/articles/13000081130-instrument\n#[derive(Clone, Serialize, Deserialize)]\n#[allow(non_snake_case)]\nstruct Instrument {\n    symbol: String,     // The contract for this position.\n    rootSymbol: String, // Root symbol for the instrument, used for grouping on the frontend.\n    state: String,      /* State of the instrument, it can be\n                         * `Open`Closed`Unlisted`Expired`Cleared. */\n    typ: String, // Type of the instrument (e.g. Futures, Perpetual Contracts).\n    listing: String,\n    front: Option<String>,\n    expiry: Option<String>,\n    settle: Option<String>,\n    listedSettle: Option<String>,\n    inverseLeg: Option<String>,\n    positionCurrency: String, /* Currency for position of this contract. If not null, 1 contract\n                               * = 1 positionCurrency. */\n    underlying: String, // Defines the underlying asset of the instrument (e.g.XBT).\n    quoteCurrency: String, // Currency of the quote price.\n    underlyingSymbol: String, // Symbol of the underlying asset.\n    reference: String,  // Venue of the reference symbol.\n    referenceSymbol: String, // Symbol of index being referenced (e.g. .BXBT).\n    calcInterval: Option<String>,\n    publishInterval: Option<String>,\n    publishTime: Option<String>,\n    maxOrderQty: i64,\n    maxPrice: f64,\n    lotSize: f64,\n    tickSize: f64,\n    multiplier: f64,\n    settlCurrency: String,\n    underlyingToPositionMultiplier: Option<f64>,\n    underlyingToSettleMultiplier: Option<f64>,\n    quoteToSettleMultiplier: Option<f64>,\n    isQuanto: bool,\n    isInverse: bool,\n    initMargin: f64,\n    maintMargin: f64,\n    riskLimit: Option<f64>,\n    riskStep: Option<i64>,\n    limit: Option<f64>,\n    capped: bool,\n    taxed: bool,\n    deleverage: bool,\n    makerFee: f64,\n    takerFee: f64,\n    settlementFee: f64,\n    insuranceFee: f64,\n    fundingBaseSymbol: String,\n    fundingQuoteSymbol: String,\n    fundingPremiumSymbol: String,\n    fundingTimestamp: Option<String>,\n    fundingInterval: Option<String>,\n    fundingRate: Option<f64>,\n    indicativeFundingRate: Option<f64>,\n    rebalanceTimestamp: Option<String>,\n    rebalanceInterval: Option<String>,\n    openingTimestamp: String,\n    closingTimestamp: String,\n    sessionInterval: String,\n    prevTotalVolume: i64,\n    totalVolume: i64,\n    volume: i64,\n    volume24h: i64,\n    prevTotalTurnover: i64,\n    totalTurnover: i64,\n    turnover: i64,\n    turnover24h: i64,\n    homeNotional24h: f64,\n    foreignNotional24h: f64,\n    lastTickDirection: String,\n    hasLiquidity: bool,\n    openInterest: i64,\n    openValue: i64,\n    fairMethod: String,\n    markMethod: String,\n    timestamp: String,\n    #[serde(flatten)]\n    extra: HashMap<String, Value>,\n}\n\nfn fetch_instruments(market_type: MarketType) -> Result<Vec<Instrument>> {\n    let text = http_get(\"https://www.bitmex.com/api/v1/instrument/active\", None)?;\n    let instruments: Vec<Instrument> = serde_json::from_str::<Vec<Instrument>>(&text)?\n        .into_iter()\n        .filter(|x| x.state == \"Open\" && x.hasLiquidity && x.volume24h > 0 && x.turnover24h > 0)\n        .collect();\n\n    let spot: Vec<Instrument> = instruments.iter().filter(|x| x.typ == \"IFXXXP\").cloned().collect();\n    let swap: Vec<Instrument> = instruments.iter().filter(|x| x.typ == \"FFWCSX\").cloned().collect();\n    let futures: Vec<Instrument> =\n        instruments.iter().filter(|x| x.typ == \"FFCCSX\").cloned().collect();\n    // let fx: Vec<Instrument> = instruments\n    //     .iter()\n    //     .filter(|x| x.typ == \"FFWCSF\")\n    //     .cloned()\n    //     .collect();\n\n    for x in swap.iter() {\n        assert_eq!(\"FundingRate\", x.fairMethod.as_str());\n        assert!(x.expiry.is_none());\n        assert!(x.symbol[x.symbol.len() - 1..].parse::<i32>().is_err());\n        if let Some(pos) = x.symbol.rfind('_') {\n            // e.g., ETHUSD_ETH\n            assert_eq!(&(x.symbol[..pos]), format!(\"{}{}\", x.underlying, x.quoteCurrency));\n        } else {\n            assert_eq!(x.symbol, format!(\"{}{}\", x.underlying, x.quoteCurrency));\n        }\n        // println!(\"{}, {}, {}, {}, {}, {}\", x.symbol, x.rootSymbol,\n        // x.quoteCurrency, x.settlCurrency, x.positionCurrency, x.underlying);\n    }\n    for x in futures.iter() {\n        assert_eq!(\"ImpactMidPrice\", x.fairMethod.as_str());\n        assert!(x.expiry.is_some());\n        if let Some(pos) = x.symbol.rfind('_') {\n            // e.g., ETHUSDM22_ETH\n            assert!(x.symbol[pos - 2..pos].parse::<i32>().is_ok());\n        } else {\n            assert!(x.symbol[x.symbol.len() - 2..].parse::<i32>().is_ok());\n        }\n    }\n    // Inverse\n    for x in instruments.iter().filter(|x| x.isInverse) {\n        assert!(x.multiplier < 0.0);\n        assert_eq!(x.quoteCurrency, x.positionCurrency);\n    }\n    // Quanto\n    for x in instruments.iter().filter(|x| x.isQuanto) {\n        assert!(x.positionCurrency.is_empty());\n        // settled in XBT, quoted in USD or USDC\n        assert_eq!(x.settlCurrency.to_uppercase(), \"XBT\");\n        if x.typ != \"FFWCSF\" {\n            assert!(x.quoteCurrency == \"USD\" || x.quoteCurrency == \"USDC\");\n        }\n    }\n    for x in instruments.iter().filter(|x| x.positionCurrency.is_empty() && x.typ != \"IFXXXP\") {\n        assert!(x.isQuanto);\n    }\n    // Linear\n    for x in instruments.iter().filter(|x| !x.isQuanto && !x.isInverse && x.typ != \"IFXXXP\") {\n        // settled in XBT, qouted in XBT\n        // or settled in USDT, qouted in USDT\n        assert_eq!(x.settlCurrency.to_uppercase(), x.quoteCurrency);\n    }\n\n    let filtered: Vec<Instrument> = match market_type {\n        MarketType::Unknown => instruments,\n        MarketType::Spot => spot,\n        MarketType::LinearSwap => {\n            swap.iter().filter(|x| !x.isQuanto && !x.isInverse).cloned().collect()\n        }\n        MarketType::InverseSwap => {\n            swap.iter().filter(|x| !x.isQuanto && x.isInverse).cloned().collect()\n        }\n        MarketType::QuantoSwap => swap.iter().filter(|x| x.isQuanto).cloned().collect(),\n        MarketType::LinearFuture => {\n            futures.iter().filter(|x| !x.isInverse && !x.isQuanto).cloned().collect()\n        }\n        MarketType::InverseFuture => futures.iter().filter(|x| x.isInverse).cloned().collect(),\n        MarketType::QuantoFuture => futures.iter().filter(|x| x.isQuanto).cloned().collect(),\n        _ => panic!(\"Unsupported market_type: {market_type}\"),\n    };\n    Ok(filtered)\n}\n"
  },
  {
    "path": "crypto-markets/src/exchanges/bitstamp.rs",
    "content": "use super::utils::http_get;\nuse crate::{error::Result, Fees, Market, MarketType, Precision};\n\nuse serde::{Deserialize, Serialize};\nuse serde_json::Value;\nuse std::collections::HashMap;\n\npub(crate) fn fetch_symbols(market_type: MarketType) -> Result<Vec<String>> {\n    match market_type {\n        MarketType::Spot => fetch_spot_symbols(),\n        _ => panic!(\"Unsupported market_type: {market_type}\"),\n    }\n}\n\npub(crate) fn fetch_markets(market_type: MarketType) -> Result<Vec<Market>> {\n    match market_type {\n        MarketType::Spot => fetch_spot_markets(),\n        _ => panic!(\"Unsupported market_type: {market_type}\"),\n    }\n}\n\n#[derive(Serialize, Deserialize)]\nstruct SpotMarket {\n    base_decimals: i64,\n    minimum_order: String,\n    name: String,\n    counter_decimals: i64,\n    trading: String,\n    url_symbol: String,\n    description: String,\n    #[serde(flatten)]\n    extra: HashMap<String, Value>,\n}\n\n// see <https://www.bitstamp.net/api/>\nfn fetch_spot_markets_raw() -> Result<Vec<SpotMarket>> {\n    let txt = http_get(\"https://www.bitstamp.net/api/v2/trading-pairs-info/\", None)?;\n    let markets = serde_json::from_str::<Vec<SpotMarket>>(&txt)?;\n    Ok(markets.into_iter().filter(|m| m.trading == \"Enabled\").collect())\n}\n\nfn fetch_spot_symbols() -> Result<Vec<String>> {\n    let symbols =\n        fetch_spot_markets_raw()?.into_iter().map(|m| m.url_symbol).collect::<Vec<String>>();\n    Ok(symbols)\n}\n\nfn fetch_spot_markets() -> Result<Vec<Market>> {\n    let markets = fetch_spot_markets_raw()?\n        .into_iter()\n        .map(|m| {\n            let info = serde_json::to_value(&m).unwrap().as_object().unwrap().clone();\n            let pair = crypto_pair::normalize_pair(&m.url_symbol, \"bitstamp\").unwrap();\n            let (base, quote) = {\n                let v: Vec<&str> = pair.split('/').collect();\n                (v[0].to_string(), v[1].to_string())\n            };\n            let (base_id, quote_id) = {\n                let v: Vec<&str> = m.name.split('/').collect();\n                (v[0].to_string(), v[1].to_string())\n            };\n            Market {\n                exchange: \"bitstamp\".to_string(),\n                market_type: MarketType::Spot,\n                symbol: m.url_symbol,\n                base_id,\n                quote_id,\n                settle_id: None,\n                base,\n                quote,\n                settle: None,\n                active: true,\n                margin: true,\n                // see https://www.bitstamp.net/fee-schedule/\n                fees: Fees { maker: 0.005, taker: 0.005 },\n                precision: Precision {\n                    tick_size: 1.0 / (10_i64.pow(m.base_decimals as u32) as f64),\n                    lot_size: 1.0 / (10_i64.pow(m.counter_decimals as u32) as f64),\n                },\n                quantity_limit: None,\n                contract_value: None,\n                delivery_date: None,\n                info,\n            }\n        })\n        .collect::<Vec<Market>>();\n    Ok(markets)\n}\n"
  },
  {
    "path": "crypto-markets/src/exchanges/bitz/bitz_spot.rs",
    "content": "use std::collections::HashMap;\n\nuse super::super::utils::http_get;\nuse crate::error::{Error, Result};\n\nuse serde::{Deserialize, Serialize};\nuse serde_json::Value;\n\n#[derive(Clone, Serialize, Deserialize)]\n#[allow(non_snake_case)]\nstruct SpotMarket {\n    id: String,\n    symbol: String,\n    baseCurrency: String,\n    quoteCurrency: String,\n    amountPrecision: String,\n    pricePrecision: String,\n    status: String,\n    minOrderAmt: String,\n    maxOrderAmt: String,\n    buyFree: String,\n    sellFree: String,\n    marketBuyFloat: String,\n    #[serde(flatten)]\n    extra: HashMap<String, Value>,\n}\n\n#[derive(Serialize, Deserialize)]\nstruct Response {\n    status: i64,\n    msg: String,\n    data: HashMap<String, SpotMarket>,\n    time: i64,\n    microtime: String,\n    source: String,\n}\n\n// See https://apidocv2.bitz.plus/en/#get-data-of-trading-pairs\nfn fetch_spot_markets_raw() -> Result<Vec<SpotMarket>> {\n    let txt = http_get(\"https://apiv2.bitz.com/V2/Market/symbolList\", None)?;\n    let resp = serde_json::from_str::<Response>(&txt)?;\n    if resp.status != 200 {\n        Err(Error(txt))\n    } else {\n        let markets =\n            resp.data.values().cloned().filter(|x| x.status == \"1\").collect::<Vec<SpotMarket>>();\n        Ok(markets)\n    }\n}\n\npub(super) fn fetch_spot_symbols() -> Result<Vec<String>> {\n    let markets = fetch_spot_markets_raw()?;\n    let symbols: Vec<String> = markets.into_iter().map(|m| m.symbol).collect();\n    Ok(symbols)\n}\n"
  },
  {
    "path": "crypto-markets/src/exchanges/bitz/bitz_swap.rs",
    "content": "use std::collections::HashMap;\n\nuse super::super::utils::http_get;\nuse crate::error::{Error, Result};\n\nuse serde::{Deserialize, Serialize};\nuse serde_json::Value;\n\n#[derive(Clone, Serialize, Deserialize)]\n#[allow(non_snake_case)]\nstruct SwapMarket {\n    contractId: String,        // contract id\n    symbol: String,            // symbol\n    settleAnchor: String,      // settle anchor\n    quoteAnchor: String,       // quote anchor\n    contractAnchor: String,    // contract anchor\n    contractValue: String,     // contract Value\n    pair: String,              //contract market\n    expiry: String,            //delivery time (non-perpetual contract)\n    maxLeverage: String,       // max leverage\n    maintanceMargin: String,   //maintenance margin\n    makerFee: String,          // maker fee rate\n    takerFee: String,          // taker fee rate\n    settleFee: String,         // settlement fee rate\n    priceDec: String,          // floating point decimal of price\n    anchorDec: String,         // floating point decimal of quote anchor\n    status: String,            // status，1: trading, 0:pending,-1:permanent stop\n    isreverse: String,         // 1:reverse contract，-1: forward contract\n    allowCross: String,        // Allow cross position，1:Yes，-1:No\n    allowLeverages: String,    // Leverage multiple allowed by the system\n    maxOrderNum: String,       // max number of unfilled orders\n    maxAmount: String,         // max amount of a single order\n    minAmount: String,         // min amount of a single order\n    maxPositionAmount: String, //max position amount\n    #[serde(flatten)]\n    extra: HashMap<String, Value>,\n}\n\n#[derive(Serialize, Deserialize)]\nstruct Response {\n    status: i64,\n    msg: String,\n    data: Vec<SwapMarket>,\n    time: i64,\n    microtime: String,\n    source: String,\n}\n\n// See https://apidocv2.bitz.plus/en/#get-market-list-of-contract-transactions\nfn fetch_swap_markets_raw() -> Result<Vec<SwapMarket>> {\n    let txt = http_get(\"https://apiv2.bitz.com/V2/Market/getContractCoin\", None)?;\n    let resp = serde_json::from_str::<Response>(&txt)?;\n    if resp.status != 200 {\n        Err(Error(txt))\n    } else {\n        let markets: Vec<SwapMarket> = resp.data.into_iter().filter(|x| x.status == \"1\").collect();\n        Ok(markets)\n    }\n}\n\npub(super) fn fetch_inverse_swap_symbols() -> Result<Vec<String>> {\n    let symbols = fetch_swap_markets_raw()?\n        .into_iter()\n        .filter(|m| m.isreverse == \"1\")\n        .map(|m| m.pair)\n        .collect::<Vec<String>>();\n    Ok(symbols)\n}\n\npub(super) fn fetch_linear_swap_symbols() -> Result<Vec<String>> {\n    let symbols = fetch_swap_markets_raw()?\n        .into_iter()\n        .filter(|m| m.isreverse == \"-1\" && m.settleAnchor == \"USDT\")\n        .map(|m| m.pair)\n        .collect::<Vec<String>>();\n    Ok(symbols)\n}\n"
  },
  {
    "path": "crypto-markets/src/exchanges/bitz/mod.rs",
    "content": "mod bitz_spot;\nmod bitz_swap;\n\nuse crate::{error::Result, Market, MarketType};\n\npub(crate) fn fetch_symbols(market_type: MarketType) -> Result<Vec<String>> {\n    match market_type {\n        MarketType::Spot => bitz_spot::fetch_spot_symbols(),\n        MarketType::InverseSwap => bitz_swap::fetch_inverse_swap_symbols(),\n        MarketType::LinearSwap => bitz_swap::fetch_linear_swap_symbols(),\n        _ => panic!(\"Unsupported market_type: {market_type}\"),\n    }\n}\n\npub(crate) fn fetch_markets(_market_type: MarketType) -> Result<Vec<Market>> {\n    Ok(Vec::new())\n}\n"
  },
  {
    "path": "crypto-markets/src/exchanges/bybit.rs",
    "content": "use std::collections::HashMap;\n\nuse super::utils::http_get;\nuse crate::{error::Result, Fees, Market, MarketType, Precision, QuantityLimit};\n\nuse chrono::{prelude::*, DateTime};\nuse serde::{Deserialize, Serialize};\nuse serde_json::Value;\n\npub(crate) fn fetch_symbols(market_type: MarketType) -> Result<Vec<String>> {\n    match market_type {\n        MarketType::InverseSwap => fetch_inverse_swap_symbols(),\n        MarketType::LinearSwap => fetch_linear_swap_symbols(),\n        MarketType::InverseFuture => fetch_inverse_future_symbols(),\n        _ => panic!(\"Unsupported market_type: {market_type}\"),\n    }\n}\n\npub(crate) fn fetch_markets(market_type: MarketType) -> Result<Vec<Market>> {\n    match market_type {\n        MarketType::InverseSwap => fetch_inverse_swap_markets(),\n        MarketType::LinearSwap => fetch_linear_swap_markets(),\n        MarketType::InverseFuture => fetch_inverse_future_markets(),\n        _ => panic!(\"Unsupported market_type: {market_type}\"),\n    }\n}\n\n#[derive(Serialize, Deserialize)]\nstruct LeverageFilter {\n    min_leverage: i64,\n    max_leverage: i64,\n    leverage_step: String,\n}\n\n#[derive(Serialize, Deserialize)]\nstruct PriceFilter {\n    min_price: String,\n    max_price: String,\n    tick_size: String,\n}\n\n#[derive(Serialize, Deserialize)]\nstruct LotSizeFilter {\n    max_trading_qty: f64,\n    min_trading_qty: f64,\n    qty_step: f64,\n}\n\n#[derive(Serialize, Deserialize)]\nstruct BybitMarket {\n    name: String,\n    alias: String,\n    status: String,\n    base_currency: String,\n    quote_currency: String,\n    price_scale: i64,\n    taker_fee: String,\n    maker_fee: String,\n    leverage_filter: LeverageFilter,\n    price_filter: PriceFilter,\n    lot_size_filter: LotSizeFilter,\n    #[serde(flatten)]\n    extra: HashMap<String, Value>,\n}\n\n#[derive(Serialize, Deserialize)]\nstruct Response {\n    ret_code: i64,\n    ret_msg: String,\n    ext_code: String,\n    ext_info: String,\n    result: Vec<BybitMarket>,\n}\n\n// See https://bybit-exchange.github.io/docs/inverse/#t-querysymbol\nfn fetch_markets_raw() -> Result<Vec<BybitMarket>> {\n    let txt = http_get(\"https://api.bybit.com/v2/public/symbols\", None)?;\n    let resp = serde_json::from_str::<Response>(&txt)?;\n    assert_eq!(resp.ret_code, 0);\n    Ok(resp.result.into_iter().filter(|m| m.status == \"Trading\").collect())\n}\n\nfn fetch_inverse_swap_symbols() -> Result<Vec<String>> {\n    let symbols = fetch_markets_raw()?\n        .into_iter()\n        .filter(|m| m.name == m.alias && m.quote_currency == \"USD\")\n        .map(|m| m.name)\n        .collect::<Vec<String>>();\n    Ok(symbols)\n}\n\nfn fetch_linear_swap_symbols() -> Result<Vec<String>> {\n    let symbols = fetch_markets_raw()?\n        .into_iter()\n        .filter(|m| m.name == m.alias && m.quote_currency == \"USDT\")\n        .map(|m| m.name)\n        .collect::<Vec<String>>();\n    Ok(symbols)\n}\n\nfn fetch_inverse_future_symbols() -> Result<Vec<String>> {\n    let symbols = fetch_markets_raw()?\n        .into_iter()\n        .filter(|m| {\n            m.quote_currency == \"USD\" && m.name[(m.name.len() - 2)..].parse::<i64>().is_ok()\n        })\n        .map(|m| m.name)\n        .collect::<Vec<String>>();\n    Ok(symbols)\n}\n\nfn to_market(raw_market: &BybitMarket) -> Market {\n    let pair = crypto_pair::normalize_pair(&raw_market.name, \"bybit\").unwrap();\n    let (base, quote) = {\n        let v: Vec<&str> = pair.split('/').collect();\n        (v[0].to_string(), v[1].to_string())\n    };\n    let delivery_date: Option<u64> =\n        if raw_market.name[(raw_market.name.len() - 2)..].parse::<i64>().is_ok() {\n            let n = raw_market.alias.len();\n            let s = raw_market.alias.as_str();\n            let month = &s[(n - 4)..(n - 2)];\n            let day = &s[(n - 2)..];\n            let now = Utc::now();\n            let year = Utc::now().year();\n            let delivery_time = DateTime::parse_from_rfc3339(\n                format!(\"{year}-{month}-{day}T00:00:00+00:00\").as_str(),\n            )\n            .unwrap();\n            let delivery_time = if delivery_time > now {\n                delivery_time\n            } else {\n                DateTime::parse_from_rfc3339(\n                    format!(\"{}-{}-{}T00:00:00+00:00\", year + 1, month, day).as_str(),\n                )\n                .unwrap()\n            };\n            assert!(delivery_time > now);\n            Some(delivery_time.timestamp_millis() as u64)\n        } else {\n            None\n        };\n    Market {\n        exchange: \"bybit\".to_string(),\n        market_type: if raw_market.name != raw_market.alias {\n            MarketType::InverseFuture\n        } else if raw_market.quote_currency == \"USDT\" {\n            MarketType::LinearSwap\n        } else {\n            MarketType::InverseSwap\n        },\n        symbol: raw_market.name.to_string(),\n        base_id: raw_market.base_currency.to_string(),\n        quote_id: raw_market.quote_currency.to_string(),\n        settle_id: if raw_market.quote_currency == \"USDT\" {\n            Some(raw_market.quote_currency.to_string())\n        } else {\n            Some(raw_market.base_currency.to_string())\n        },\n        base,\n        quote,\n        settle: if raw_market.quote_currency == \"USDT\" {\n            Some(raw_market.quote_currency.to_string())\n        } else {\n            Some(raw_market.base_currency.to_string())\n        },\n        active: raw_market.status == \"Trading\",\n        margin: true,\n        fees: Fees {\n            maker: raw_market.maker_fee.parse::<f64>().unwrap(),\n            taker: raw_market.taker_fee.parse::<f64>().unwrap(),\n        },\n        precision: Precision {\n            tick_size: raw_market.price_filter.tick_size.parse::<f64>().unwrap(),\n            lot_size: raw_market.lot_size_filter.qty_step,\n        },\n        quantity_limit: Some(QuantityLimit {\n            min: Some(raw_market.lot_size_filter.min_trading_qty),\n            max: Some(raw_market.lot_size_filter.max_trading_qty),\n            notional_min: None,\n            notional_max: None,\n        }),\n        contract_value: Some(1.0),\n        delivery_date,\n        info: serde_json::to_value(raw_market).unwrap().as_object().unwrap().clone(),\n    }\n}\n\nfn fetch_inverse_swap_markets() -> Result<Vec<Market>> {\n    let markets = fetch_markets_raw()?\n        .into_iter()\n        .filter(|m| m.name == m.alias && m.quote_currency == \"USD\")\n        .map(|m| to_market(&m))\n        .collect::<Vec<Market>>();\n    Ok(markets)\n}\n\nfn fetch_linear_swap_markets() -> Result<Vec<Market>> {\n    let markets = fetch_markets_raw()?\n        .into_iter()\n        .filter(|m| m.name == m.alias && m.quote_currency == \"USDT\")\n        .map(|m| to_market(&m))\n        .collect::<Vec<Market>>();\n    Ok(markets)\n}\n\nfn fetch_inverse_future_markets() -> Result<Vec<Market>> {\n    let markets = fetch_markets_raw()?\n        .into_iter()\n        .filter(|m| {\n            m.quote_currency == \"USD\" && m.name[(m.name.len() - 2)..].parse::<i64>().is_ok()\n        })\n        .map(|m| to_market(&m))\n        .collect::<Vec<Market>>();\n    Ok(markets)\n}\n"
  },
  {
    "path": "crypto-markets/src/exchanges/coinbase_pro.rs",
    "content": "use std::collections::HashMap;\n\nuse super::utils::http_get;\nuse crate::{error::Result, Fees, Market, MarketType, Precision, QuantityLimit};\n\nuse serde::{Deserialize, Serialize};\nuse serde_json::Value;\n\npub(crate) fn fetch_symbols(market_type: MarketType) -> Result<Vec<String>> {\n    match market_type {\n        MarketType::Spot => fetch_spot_symbols(),\n        _ => panic!(\"Unsupported market_type: {market_type}\"),\n    }\n}\n\npub(crate) fn fetch_markets(market_type: MarketType) -> Result<Vec<Market>> {\n    match market_type {\n        MarketType::Spot => fetch_spot_markets(),\n        _ => panic!(\"Unsupported market_type: {market_type}\"),\n    }\n}\n\n#[derive(Serialize, Deserialize)]\nstruct SpotMarket {\n    id: String,\n    base_currency: String,\n    quote_currency: String,\n    quote_increment: String,\n    base_increment: String,\n    display_name: String,\n    min_market_funds: Option<String>,\n    max_market_funds: Option<String>,\n    margin_enabled: bool,\n    post_only: bool,\n    limit_only: bool,\n    cancel_only: bool,\n    trading_disabled: bool,\n    status: String,\n    status_message: String,\n    #[serde(flatten)]\n    extra: HashMap<String, Value>,\n}\n\n// see <https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getproducts>\nfn fetch_spot_markets_raw() -> Result<Vec<SpotMarket>> {\n    let txt = http_get(\"https://api.exchange.coinbase.com/products\", None)?;\n    let markets = serde_json::from_str::<Vec<SpotMarket>>(&txt)?;\n    Ok(markets)\n}\n\nfn fetch_spot_symbols() -> Result<Vec<String>> {\n    let symbols = fetch_spot_markets_raw()?\n        .into_iter()\n        .filter(|m| !m.trading_disabled && m.status == \"online\" && !m.cancel_only)\n        .map(|m| m.id)\n        .collect::<Vec<String>>();\n    Ok(symbols)\n}\n\nfn fetch_spot_markets() -> Result<Vec<Market>> {\n    let markets = fetch_spot_markets_raw()?\n        .into_iter()\n        .map(|m| {\n            let info = serde_json::to_value(&m).unwrap().as_object().unwrap().clone();\n            let pair = crypto_pair::normalize_pair(&m.id, \"coinbase_pro\").unwrap();\n            let (base, quote) = {\n                let v: Vec<&str> = pair.split('/').collect();\n                (v[0].to_string(), v[1].to_string())\n            };\n            Market {\n                exchange: \"coinbase_pro\".to_string(),\n                market_type: MarketType::Spot,\n                symbol: m.id,\n                base_id: m.base_currency,\n                quote_id: m.quote_currency,\n                settle_id: None,\n                base,\n                quote,\n                settle: None,\n                active: !m.trading_disabled && m.status == \"online\" && !m.cancel_only,\n                margin: m.margin_enabled,\n                // // see https://pro.coinbase.com/fees, https://pro.coinbase.com/orders/fees\n                fees: Fees { maker: 0.005, taker: 0.005 },\n                precision: Precision {\n                    tick_size: m.quote_increment.parse::<f64>().unwrap(),\n                    lot_size: m.base_increment.parse::<f64>().unwrap(),\n                },\n                quantity_limit: Some(QuantityLimit {\n                    min: None,\n                    max: None,\n                    notional_min: m.min_market_funds.map(|x| x.parse::<f64>().unwrap()),\n                    notional_max: m.max_market_funds.map(|x| x.parse::<f64>().unwrap()),\n                }),\n                contract_value: None,\n                delivery_date: None,\n                info,\n            }\n        })\n        .collect::<Vec<Market>>();\n    Ok(markets)\n}\n"
  },
  {
    "path": "crypto-markets/src/exchanges/deribit/mod.rs",
    "content": "mod utils;\n\nuse crate::{error::Result, Market, MarketType};\n\npub(crate) fn fetch_symbols(market_type: MarketType) -> Result<Vec<String>> {\n    match market_type {\n        MarketType::InverseFuture => utils::fetch_inverse_future_symbols(),\n        MarketType::InverseSwap => utils::fetch_inverse_swap_symbols(),\n        MarketType::EuropeanOption => utils::fetch_option_symbols(),\n        _ => panic!(\"Unsupported market_type: {market_type}\"),\n    }\n}\n\npub(crate) fn fetch_markets(market_type: MarketType) -> Result<Vec<Market>> {\n    match market_type {\n        MarketType::InverseFuture => utils::fetch_inverse_future_markets(),\n        MarketType::InverseSwap => utils::fetch_inverse_swap_markets(),\n        MarketType::EuropeanOption => utils::fetch_option_markets(),\n        _ => panic!(\"Unsupported market_type: {market_type}\"),\n    }\n}\n"
  },
  {
    "path": "crypto-markets/src/exchanges/deribit/utils.rs",
    "content": "use super::super::utils::http_get;\nuse crate::{\n    error::{Error, Result},\n    market::{Fees, Precision, QuantityLimit},\n    Market,\n};\n\nuse crypto_market_type::MarketType;\nuse serde::{Deserialize, Serialize};\nuse serde_json::Value;\nuse std::collections::HashMap;\n\nfn check_error_in_body(body: String) -> Result<String> {\n    let obj = serde_json::from_str::<HashMap<String, Value>>(&body).unwrap();\n    if obj.contains_key(\"error\") { Err(Error(body)) } else { Ok(body) }\n}\n\npub(super) fn deribit_http_get(url: &str) -> Result<String> {\n    let ret = http_get(url, None);\n    match ret {\n        Ok(body) => check_error_in_body(body),\n        Err(_) => ret,\n    }\n}\n\n#[derive(Serialize, Deserialize)]\n#[allow(non_snake_case)]\nstruct DeribitResponse<T> {\n    id: Option<i64>,\n    jsonrpc: String,\n    result: Vec<T>,\n    usIn: i64,\n    usOut: i64,\n    usDiff: i64,\n    testnet: bool,\n\n    #[serde(flatten)]\n    extra: HashMap<String, Value>,\n}\n\n#[derive(Serialize, Deserialize)]\nstruct Instrument {\n    tick_size: f64,\n    taker_commission: f64,\n    strike: Option<f64>,\n    settlement_period: String,\n    quote_currency: String,\n    min_trade_amount: f64,\n    max_liquidation_commission: Option<f64>,\n    max_leverage: Option<i64>,\n    maker_commission: f64,\n    kind: String,\n    is_active: bool,\n    instrument_name: String,\n    expiration_timestamp: u64,\n    creation_timestamp: u64,\n    contract_size: f64, // TODO: why i64 panic ?\n    block_trade_commission: f64,\n    base_currency: String,\n\n    #[serde(flatten)]\n    extra: HashMap<String, Value>,\n}\n\n/// Get active trading instruments.\n///\n/// doc: <https://docs.deribit.com/?shell#public-get_instruments>\n///\n/// `currency`, available values are `BTC` and `ETH`.\n///\n/// `kind`, available values are `future` and `option`.\n///\n/// Example: <https://www.deribit.com/api/v2/public/get_instruments?currency=BTC&kind=future>\nfn fetch_instruments(currency: &str, kind: &str) -> Result<Vec<Instrument>> {\n    let url = format!(\n        \"https://www.deribit.com/api/v2/public/get_instruments?currency={currency}&kind={kind}\"\n    );\n    let txt = deribit_http_get(&url)?;\n    let resp = serde_json::from_str::<DeribitResponse<Instrument>>(&txt)?;\n    Ok(resp.result)\n}\n\nfn fetch_raw_markets(kind: &str) -> Result<Vec<Instrument>> {\n    let mut all_markets: Vec<Instrument> = Vec::new();\n\n    let result = fetch_instruments(\"BTC\", kind);\n    match result {\n        Ok(mut instruments) => {\n            all_markets.append(&mut instruments);\n        }\n        Err(error) => {\n            return Err(error);\n        }\n    }\n\n    let result = fetch_instruments(\"ETH\", kind);\n    match result {\n        Ok(mut instruments) => {\n            all_markets.append(&mut instruments);\n        }\n        Err(error) => {\n            return Err(error);\n        }\n    }\n\n    Ok(all_markets.into_iter().filter(|x| x.is_active).collect())\n}\n\nfn fetch_symbols(kind: &str) -> Result<Vec<String>> {\n    let all_markets = fetch_raw_markets(kind)?;\n    let all_symbols: Vec<String> = all_markets.into_iter().map(|x| x.instrument_name).collect();\n    Ok(all_symbols)\n}\n\npub(super) fn fetch_inverse_future_symbols() -> Result<Vec<String>> {\n    let result = fetch_symbols(\"future\");\n    match result {\n        Ok(symbols) => Ok(symbols.into_iter().filter(|x| !x.ends_with(\"-PERPETUAL\")).collect()),\n        Err(error) => Err(error),\n    }\n}\n\npub(super) fn fetch_inverse_swap_symbols() -> Result<Vec<String>> {\n    let result = fetch_symbols(\"future\");\n    match result {\n        Ok(symbols) => Ok(symbols.into_iter().filter(|x| x.ends_with(\"-PERPETUAL\")).collect()),\n        Err(error) => Err(error),\n    }\n}\n\npub(super) fn fetch_option_symbols() -> Result<Vec<String>> {\n    fetch_symbols(\"option\")\n}\n\nfn to_market(raw_market: &Instrument) -> Market {\n    let market_type = if raw_market.kind == \"future\" {\n        if raw_market.instrument_name.ends_with(\"-PERPETUAL\") {\n            MarketType::InverseSwap\n        } else {\n            MarketType::InverseFuture\n        }\n    } else {\n        MarketType::EuropeanOption\n    };\n    let pair = crypto_pair::normalize_pair(&raw_market.instrument_name, \"deribit\").unwrap();\n    let (base, quote) = {\n        let v: Vec<&str> = pair.split('/').collect();\n        (v[0].to_string(), v[1].to_string())\n    };\n    Market {\n        exchange: \"deribit\".to_string(),\n        market_type,\n        symbol: raw_market.instrument_name.to_string(),\n        base_id: raw_market.base_currency.to_string(),\n        quote_id: raw_market.quote_currency.to_string(),\n        settle_id: Some(raw_market.base_currency.to_string()),\n        base: base.clone(),\n        quote,\n        settle: Some(base),\n        active: raw_market.is_active,\n        margin: true,\n        fees: Fees { maker: raw_market.maker_commission, taker: raw_market.taker_commission },\n        precision: Precision {\n            tick_size: raw_market.tick_size,\n            lot_size: raw_market.min_trade_amount,\n        },\n        quantity_limit: Some(QuantityLimit {\n            min: Some(raw_market.min_trade_amount),\n            max: None,\n            notional_min: None,\n            notional_max: None,\n        }),\n        contract_value: Some(raw_market.contract_size),\n        delivery_date: if market_type == MarketType::InverseSwap {\n            None\n        } else {\n            Some(raw_market.expiration_timestamp)\n        },\n        info: serde_json::to_value(raw_market).unwrap().as_object().unwrap().clone(),\n    }\n}\n\npub(super) fn fetch_inverse_future_markets() -> Result<Vec<Market>> {\n    let raw_markets = fetch_raw_markets(\"future\")?;\n    let markets: Vec<Market> = raw_markets\n        .into_iter()\n        .filter(|x| !x.instrument_name.ends_with(\"-PERPETUAL\"))\n        .map(|x| to_market(&x))\n        .collect();\n    Ok(markets)\n}\n\npub(super) fn fetch_inverse_swap_markets() -> Result<Vec<Market>> {\n    let raw_markets = fetch_raw_markets(\"future\")?;\n    let markets: Vec<Market> = raw_markets\n        .into_iter()\n        .filter(|x| x.instrument_name.ends_with(\"-PERPETUAL\"))\n        .map(|x| to_market(&x))\n        .collect();\n    Ok(markets)\n}\n\npub(super) fn fetch_option_markets() -> Result<Vec<Market>> {\n    let raw_markets = fetch_raw_markets(\"option\")?;\n    let markets: Vec<Market> = raw_markets.into_iter().map(|x| to_market(&x)).collect();\n    Ok(markets)\n}\n"
  },
  {
    "path": "crypto-markets/src/exchanges/dydx/dydx_swap.rs",
    "content": "use std::collections::HashMap;\n\nuse super::super::utils::http_get;\nuse crate::{error::Result, Fees, Market, Precision, QuantityLimit};\n\nuse crypto_market_type::MarketType;\nuse serde::{Deserialize, Serialize};\nuse serde_json::Value;\n\nconst BASE_URL: &str = \"https://api.dydx.exchange\";\n\n#[derive(Clone, Serialize, Deserialize)]\n#[allow(non_snake_case)]\nstruct PerpetualMarket {\n    market: String,\n    status: String,\n    baseAsset: String,\n    quoteAsset: String,\n    stepSize: String,\n    tickSize: String,\n    minOrderSize: String,\n    #[serde(rename = \"type\")]\n    type_: String,\n    #[serde(flatten)]\n    extra: HashMap<String, Value>,\n}\n\n#[derive(Clone, Serialize, Deserialize)]\n#[allow(non_snake_case)]\nstruct MarketsResponse {\n    markets: HashMap<String, PerpetualMarket>,\n}\n\n// See https://docs.dydx.exchange/#get-markets\nfn fetch_markets_raw() -> Result<Vec<PerpetualMarket>> {\n    let txt = http_get(format!(\"{BASE_URL}/v3/markets\").as_str(), None)?;\n    let resp = serde_json::from_str::<MarketsResponse>(&txt)?;\n    Ok(resp\n        .markets\n        .values()\n        .cloned()\n        .filter(|x| x.status == \"ONLINE\")\n        .collect::<Vec<PerpetualMarket>>())\n}\n\npub(super) fn fetch_linear_swap_symbols() -> Result<Vec<String>> {\n    let markets = fetch_markets_raw()?;\n    let symbols = markets.into_iter().map(|m| m.market).collect::<Vec<String>>();\n    Ok(symbols)\n}\n\npub(super) fn fetch_linear_swap_markets() -> Result<Vec<Market>> {\n    let markets = fetch_markets_raw()?\n        .into_iter()\n        .map(|m| {\n            let info = serde_json::to_value(&m).unwrap().as_object().unwrap().clone();\n            let pair = crypto_pair::normalize_pair(&m.market, \"dydx\").unwrap();\n            let (base, quote) = {\n                let v: Vec<&str> = pair.split('/').collect();\n                (v[0].to_string(), v[1].to_string())\n            };\n            Market {\n                exchange: \"dydx\".to_string(),\n                market_type: MarketType::LinearSwap,\n                symbol: m.market,\n                base_id: m.baseAsset,\n                quote_id: m.quoteAsset,\n                settle_id: Some(quote.clone()),\n                base,\n                quote: quote.clone(),\n                settle: Some(quote),\n                active: m.status == \"ONLINE\",\n                margin: true,\n                // see https://trade.dydx.exchange/portfolio/fees\n                fees: Fees { maker: 0.0005, taker: 0.0001 },\n                precision: Precision {\n                    tick_size: m.tickSize.parse::<f64>().unwrap(),\n                    lot_size: m.stepSize.parse::<f64>().unwrap(),\n                },\n                quantity_limit: Some(QuantityLimit {\n                    min: m.minOrderSize.parse::<f64>().ok(),\n                    max: None,\n                    notional_min: None,\n                    notional_max: None,\n                }),\n                contract_value: Some(1.0),\n                delivery_date: None,\n                info,\n            }\n        })\n        .collect::<Vec<Market>>();\n    Ok(markets)\n}\n"
  },
  {
    "path": "crypto-markets/src/exchanges/dydx/mod.rs",
    "content": "mod dydx_swap;\n\nuse crate::{error::Result, Market, MarketType};\n\npub(crate) fn fetch_symbols(market_type: MarketType) -> Result<Vec<String>> {\n    match market_type {\n        MarketType::LinearSwap => dydx_swap::fetch_linear_swap_symbols(),\n        _ => panic!(\"dydX does NOT have the {market_type} market\"),\n    }\n}\n\npub(crate) fn fetch_markets(market_type: MarketType) -> Result<Vec<Market>> {\n    match market_type {\n        MarketType::LinearSwap => dydx_swap::fetch_linear_swap_markets(),\n        _ => panic!(\"dydX does NOT have the {market_type} market\"),\n    }\n}\n"
  },
  {
    "path": "crypto-markets/src/exchanges/ftx.rs",
    "content": "use std::collections::HashMap;\n\nuse super::utils::http_get;\nuse crate::{error::Result, Fees, Market, MarketType, Precision};\n\nuse chrono::{prelude::*, DateTime};\nuse serde::{Deserialize, Serialize};\nuse serde_json::Value;\n\npub(crate) fn fetch_symbols(market_type: MarketType) -> Result<Vec<String>> {\n    match market_type {\n        MarketType::Spot => fetch_spot_symbols(),\n        MarketType::LinearSwap => fetch_linear_swap_symbols(),\n        MarketType::LinearFuture => fetch_linear_future_symbols(),\n        MarketType::Move => fetch_move_symbols(),\n        MarketType::BVOL => fetch_bvol_symbols(),\n        _ => panic!(\"Unsupported market_type: {market_type}\"),\n    }\n}\n\npub(crate) fn fetch_markets(market_type: MarketType) -> Result<Vec<Market>> {\n    match market_type {\n        MarketType::Spot => fetch_spot_markets(),\n        MarketType::LinearSwap => fetch_linear_swap_markets(),\n        MarketType::LinearFuture => fetch_linear_future_markets(),\n        MarketType::Move => fetch_move_markets(),\n        MarketType::BVOL => fetch_bvol_markets(),\n        _ => panic!(\"Unsupported market_type: {market_type}\"),\n    }\n}\n\n#[derive(Serialize, Deserialize)]\n#[allow(non_snake_case)]\nstruct FtxMarket {\n    name: String,\n    baseCurrency: Option<String>,\n    quoteCurrency: Option<String>,\n    #[serde(rename = \"type\")]\n    type_: String,\n    underlying: Option<String>,\n    enabled: bool,\n    postOnly: bool,\n    priceIncrement: f64,\n    sizeIncrement: f64,\n    restricted: bool,\n    #[serde(flatten)]\n    extra: HashMap<String, Value>,\n}\n\n#[derive(Serialize, Deserialize)]\nstruct Response {\n    success: bool,\n    result: Vec<FtxMarket>,\n}\n\nfn fetch_markets_raw() -> Result<Vec<FtxMarket>> {\n    let txt = http_get(\"https://ftx.com/api/markets\", None)?;\n    let resp = serde_json::from_str::<Response>(&txt)?;\n    assert!(resp.success);\n    let valid: Vec<FtxMarket> = resp.result.into_iter().filter(|x| x.enabled).collect();\n    Ok(valid)\n}\n\nfn fetch_spot_symbols() -> Result<Vec<String>> {\n    let markets = fetch_markets_raw()?;\n    let symbols: Vec<String> = markets\n        .into_iter()\n        .filter(|x| x.type_ == \"spot\" && !x.name.contains(\"BVOL/\"))\n        .map(|x| x.name)\n        .collect();\n    Ok(symbols)\n}\n\nfn fetch_linear_swap_symbols() -> Result<Vec<String>> {\n    let markets = fetch_markets_raw()?;\n    let symbols: Vec<String> = markets\n        .into_iter()\n        .filter(|x| x.type_ == \"future\" && x.name.ends_with(\"-PERP\"))\n        .map(|x| x.name)\n        .collect();\n    Ok(symbols)\n}\n\nfn fetch_linear_future_symbols() -> Result<Vec<String>> {\n    let markets = fetch_markets_raw()?;\n    let symbols: Vec<String> = markets\n        .into_iter()\n        .filter(|x| {\n            x.type_ == \"future\"\n                && !x.name.ends_with(\"-PERP\")\n                && !x.name.contains(\"-MOVE-\")\n                && x.name[(x.name.len() - 4)..].parse::<u32>().is_ok()\n                && x.name.contains('-')\n        })\n        .map(|x| x.name)\n        .collect();\n    Ok(symbols)\n}\n\nfn fetch_move_symbols() -> Result<Vec<String>> {\n    let markets = fetch_markets_raw()?;\n    let symbols: Vec<String> = markets\n        .into_iter()\n        .filter(|x| x.type_ == \"future\" && x.name.contains(\"-MOVE-\"))\n        .map(|x| x.name)\n        .collect();\n    Ok(symbols)\n}\n\nfn fetch_bvol_symbols() -> Result<Vec<String>> {\n    let markets = fetch_markets_raw()?;\n    let symbols: Vec<String> = markets\n        .into_iter()\n        .filter(|x| x.type_ == \"spot\" && x.name.contains(\"BVOL/\"))\n        .map(|x| x.name)\n        .collect();\n    Ok(symbols)\n}\n\nfn to_market(raw_market: &FtxMarket) -> Market {\n    let pair = crypto_pair::normalize_pair(&raw_market.name, \"ftx\").unwrap();\n    let (base, quote) = {\n        let v: Vec<&str> = pair.split('/').collect();\n        (v[0].to_string(), v[1].to_string())\n    };\n    let market_type = if raw_market.type_ == \"spot\" {\n        if raw_market.name.contains(\"BVOL/\") { MarketType::BVOL } else { MarketType::Spot }\n    } else if raw_market.type_ == \"future\" {\n        if raw_market.name.ends_with(\"-PERP\") {\n            MarketType::LinearSwap\n        } else if raw_market.name.contains(\"-MOVE-\") {\n            MarketType::Move\n        } else {\n            MarketType::LinearFuture\n        }\n    } else {\n        panic!(\"Unsupported type: {}\", raw_market.type_);\n    };\n    let delivery_date: Option<u64> =\n        if raw_market.name[(raw_market.name.len() - 4)..].parse::<u32>().is_ok() {\n            let n = raw_market.name.len();\n            let s = raw_market.name.as_str();\n            let month = &s[(n - 4)..(n - 2)];\n            let day = &s[(n - 2)..];\n            let now = Utc::now();\n            let year = Utc::now().year();\n            let delivery_time = DateTime::parse_from_rfc3339(\n                format!(\"{year}-{month}-{day}T00:00:00+00:00\").as_str(),\n            )\n            .unwrap();\n            let delivery_time = if delivery_time > now {\n                delivery_time\n            } else {\n                DateTime::parse_from_rfc3339(\n                    format!(\"{}-{}-{}T00:00:00+00:00\", year + 1, month, day).as_str(),\n                )\n                .unwrap()\n            };\n            assert!(delivery_time > now);\n            Some(delivery_time.timestamp_millis() as u64)\n        } else {\n            None\n        };\n    Market {\n        exchange: \"ftx\".to_string(),\n        market_type,\n        symbol: raw_market.name.to_string(),\n        base_id: if raw_market.type_ == \"spot\" {\n            raw_market.baseCurrency.clone().unwrap()\n        } else {\n            raw_market.underlying.clone().unwrap()\n        },\n        quote_id: if raw_market.type_ == \"spot\" {\n            raw_market.quoteCurrency.clone().unwrap()\n        } else {\n            \"USD\".to_string()\n        },\n        settle_id: if raw_market.type_ == \"spot\" { None } else { Some(\"USD\".to_string()) },\n        base,\n        quote,\n        settle: if raw_market.type_ == \"spot\" { None } else { Some(\"USD\".to_string()) },\n        active: raw_market.enabled,\n        margin: true,\n        // see https://help.ftx.com/hc/en-us/articles/360024479432-Fees\n        fees: Fees { maker: 0.0002, taker: 0.0007 },\n        precision: Precision {\n            tick_size: raw_market.priceIncrement,\n            lot_size: raw_market.sizeIncrement,\n        },\n        quantity_limit: None,\n        contract_value: if raw_market.type_ == \"spot\" { None } else { Some(1.0) },\n        delivery_date,\n        info: serde_json::to_value(raw_market).unwrap().as_object().unwrap().clone(),\n    }\n}\n\nfn fetch_spot_markets() -> Result<Vec<Market>> {\n    let markets: Vec<Market> = fetch_markets_raw()?\n        .into_iter()\n        .filter(|x| x.type_ == \"spot\")\n        .map(|x| to_market(&x))\n        .collect();\n    Ok(markets)\n}\n\nfn fetch_linear_swap_markets() -> Result<Vec<Market>> {\n    let markets = fetch_markets_raw()?;\n    let symbols: Vec<Market> = markets\n        .into_iter()\n        .filter(|x| x.type_ == \"future\" && x.name.ends_with(\"-PERP\"))\n        .map(|x| to_market(&x))\n        .collect();\n    Ok(symbols)\n}\n\nfn fetch_linear_future_markets() -> Result<Vec<Market>> {\n    let markets: Vec<Market> = fetch_markets_raw()?\n        .into_iter()\n        .filter(|x| {\n            x.type_ == \"future\"\n                && !x.name.ends_with(\"-PERP\")\n                && !x.name.contains(\"-MOVE-\")\n                && x.name[(x.name.len() - 4)..].parse::<u32>().is_ok()\n                && x.name.contains('-')\n        })\n        .map(|x| to_market(&x))\n        .collect();\n    Ok(markets)\n}\n\nfn fetch_move_markets() -> Result<Vec<Market>> {\n    let markets: Vec<Market> = fetch_markets_raw()?\n        .into_iter()\n        .filter(|x| x.type_ == \"future\" && x.name.contains(\"-MOVE-\"))\n        .map(|x| to_market(&x))\n        .collect();\n    Ok(markets)\n}\n\nfn fetch_bvol_markets() -> Result<Vec<Market>> {\n    let markets: Vec<Market> = fetch_markets_raw()?\n        .into_iter()\n        .filter(|x| x.type_ == \"spot\" && x.name.contains(\"BVOL/\"))\n        .map(|x| to_market(&x))\n        .collect();\n    Ok(markets)\n}\n"
  },
  {
    "path": "crypto-markets/src/exchanges/gate/gate_future.rs",
    "content": "use std::collections::HashMap;\n\nuse super::super::utils::http_get;\nuse crate::{error::Result, Fees, Market, Precision, QuantityLimit};\n\nuse crypto_market_type::MarketType;\nuse serde::{Deserialize, Serialize};\nuse serde_json::Value;\n\n// https://www.gateio.pro/docs/apiv4/zh_CN/#deliverycontract\n#[derive(Clone, Serialize, Deserialize)]\n#[allow(non_snake_case)]\nstruct FutureMarket {\n    name: String,\n    underlying: String,\n    cycle: String,\n    #[serde(rename = \"type\")]\n    type_: String, // inverse, direct\n    quanto_multiplier: String,\n    leverage_min: String,\n    leverage_max: String,\n    maintenance_rate: String,\n    mark_type: String, // internal, index\n    maker_fee_rate: String,\n    taker_fee_rate: String,\n    order_price_round: String,\n    mark_price_round: String,\n    settle_fee_rate: String,\n    expire_time: u64,\n    order_size_min: f64,\n    order_size_max: f64,\n    in_delisting: bool,\n    #[serde(flatten)]\n    extra: HashMap<String, Value>,\n}\n\n// See https://www.gateio.pro/docs/apiv4/zh_CN/index.html#595cd9fe3c-2\nfn fetch_future_markets_raw(settle: &str) -> Result<Vec<FutureMarket>> {\n    let txt = http_get(\n        format!(\"https://api.gateio.ws/api/v4/delivery/{settle}/contracts\").as_str(),\n        None,\n    )?;\n    let markets = serde_json::from_str::<Vec<FutureMarket>>(&txt)?;\n    Ok(markets.into_iter().filter(|x| !x.in_delisting).collect::<Vec<FutureMarket>>())\n}\n\npub(super) fn fetch_inverse_future_symbols() -> Result<Vec<String>> {\n    let symbols =\n        fetch_future_markets_raw(\"btc\")?.into_iter().map(|m| m.name).collect::<Vec<String>>();\n    Ok(symbols)\n}\n\npub(super) fn fetch_linear_future_symbols() -> Result<Vec<String>> {\n    let symbols =\n        fetch_future_markets_raw(\"usdt\")?.into_iter().map(|m| m.name).collect::<Vec<String>>();\n    Ok(symbols)\n}\n\nfn to_market(raw_market: &FutureMarket) -> Market {\n    let pair = crypto_pair::normalize_pair(&raw_market.name, \"gate\").unwrap();\n    let (base, quote) = {\n        let v: Vec<&str> = pair.split('/').collect();\n        (v[0].to_string(), v[1].to_string())\n    };\n    let (base_id, quote_id) = {\n        let v: Vec<&str> = raw_market.underlying.split('_').collect();\n        (v[0].to_string(), v[1].to_string())\n    };\n    let market_type = if raw_market.type_ == \"inverse\" {\n        MarketType::InverseFuture\n    } else if raw_market.type_ == \"direct\" {\n        MarketType::LinearFuture\n    } else {\n        panic!(\"Unsupported type: {}\", raw_market.type_);\n    };\n    let mut quanto_multiplier = raw_market.quanto_multiplier.parse::<f64>().unwrap();\n    if raw_market.underlying == \"BTC_USD\" {\n        assert_eq!(quanto_multiplier, 0.0);\n        quanto_multiplier = 1.0;\n    }\n    assert!(quanto_multiplier > 0.0);\n\n    Market {\n        exchange: \"gate\".to_string(),\n        market_type,\n        symbol: raw_market.name.to_string(),\n        base_id: base_id.clone(),\n        quote_id: quote_id.clone(),\n        settle_id: if market_type == MarketType::InverseFuture {\n            Some(base_id)\n        } else {\n            Some(quote_id)\n        },\n        base: base.clone(),\n        quote: quote.clone(),\n        settle: if market_type == MarketType::InverseFuture { Some(base) } else { Some(quote) },\n        active: !raw_market.in_delisting,\n        margin: true,\n        fees: Fees {\n            maker: raw_market.maker_fee_rate.parse::<f64>().unwrap(),\n            taker: raw_market.taker_fee_rate.parse::<f64>().unwrap(),\n        },\n        precision: Precision {\n            tick_size: raw_market.order_price_round.parse::<f64>().unwrap(),\n            lot_size: quanto_multiplier,\n        },\n        quantity_limit: Some(QuantityLimit {\n            min: Some(raw_market.order_size_min),\n            max: Some(raw_market.order_size_max),\n            notional_min: None,\n            notional_max: None,\n        }),\n        contract_value: Some(quanto_multiplier),\n        delivery_date: Some(raw_market.expire_time * 1000),\n        info: serde_json::to_value(raw_market).unwrap().as_object().unwrap().clone(),\n    }\n}\n\npub(super) fn fetch_inverse_future_markets() -> Result<Vec<Market>> {\n    let markets = fetch_future_markets_raw(\"btc\")?\n        .into_iter()\n        .map(|m| to_market(&m))\n        .collect::<Vec<Market>>();\n    Ok(markets)\n}\n\npub(super) fn fetch_linear_future_markets() -> Result<Vec<Market>> {\n    let markets = fetch_future_markets_raw(\"usdt\")?\n        .into_iter()\n        .map(|m| to_market(&m))\n        .collect::<Vec<Market>>();\n    Ok(markets)\n}\n"
  },
  {
    "path": "crypto-markets/src/exchanges/gate/gate_spot.rs",
    "content": "use std::collections::HashMap;\n\nuse super::super::utils::http_get;\nuse crate::{error::Result, Fees, Market, Precision, QuantityLimit};\n\nuse crypto_market_type::MarketType;\nuse serde::{Deserialize, Serialize};\nuse serde_json::Value;\n\n// https://www.gateio.pro/docs/apiv4/zh_CN/#currencypair\n#[derive(Serialize, Deserialize)]\n#[allow(non_snake_case)]\nstruct SpotMarket {\n    id: String,\n    base: String,\n    quote: String,\n    fee: String,\n    min_base_amount: Option<String>,\n    min_quote_amount: Option<String>,\n    amount_precision: i64,\n    precision: i64,\n    trade_status: String,\n    #[serde(flatten)]\n    extra: HashMap<String, Value>,\n}\n\n// See https://www.gateio.pro/docs/apiv4/zh_CN/index.html#611e43ef81\nfn fetch_spot_markets_raw() -> Result<Vec<SpotMarket>> {\n    let txt = http_get(\"https://api.gateio.ws/api/v4/spot/currency_pairs\", None)?;\n    let markets = serde_json::from_str::<Vec<SpotMarket>>(&txt)?;\n    Ok(markets.into_iter().filter(|x| x.trade_status == \"tradable\").collect::<Vec<SpotMarket>>())\n}\n\npub(super) fn fetch_spot_symbols() -> Result<Vec<String>> {\n    let markets = fetch_spot_markets_raw()?;\n    let symbols: Vec<String> = markets.into_iter().map(|m| m.id).collect();\n    Ok(symbols)\n}\n\npub(super) fn fetch_spot_markets() -> Result<Vec<Market>> {\n    let markets: Vec<Market> = fetch_spot_markets_raw()?\n        .into_iter()\n        .map(|raw_market| {\n            let info = serde_json::to_value(&raw_market).unwrap().as_object().unwrap().clone();\n            let pair = crypto_pair::normalize_pair(&raw_market.id, \"gate\").unwrap();\n            let (base, quote) = {\n                let v: Vec<&str> = pair.split('/').collect();\n                (v[0].to_string(), v[1].to_string())\n            };\n\n            Market {\n                exchange: \"gate\".to_string(),\n                market_type: MarketType::Spot,\n                symbol: raw_market.id.to_string(),\n                base_id: raw_market.base,\n                settle_id: None,\n                quote_id: raw_market.quote,\n                base,\n                quote,\n                settle: None,\n                active: raw_market.trade_status == \"tradable\",\n                margin: false,\n                fees: Fees {\n                    maker: raw_market.fee.parse::<f64>().unwrap() / 100_f64,\n                    taker: raw_market.fee.parse::<f64>().unwrap() / 100_f64,\n                },\n                precision: Precision {\n                    tick_size: 1.0 / (10_i64.pow(raw_market.precision as u32) as f64),\n                    lot_size: 1.0 / (10_i64.pow(raw_market.amount_precision as u32) as f64),\n                },\n                quantity_limit: raw_market.min_base_amount.map(|min_base_amount| QuantityLimit {\n                    min: min_base_amount.parse::<f64>().ok(),\n                    max: None,\n                    notional_min: None,\n                    notional_max: None,\n                }),\n                contract_value: None,\n                delivery_date: None,\n                info,\n            }\n        })\n        .collect();\n    Ok(markets)\n}\n"
  },
  {
    "path": "crypto-markets/src/exchanges/gate/gate_swap.rs",
    "content": "use std::collections::HashMap;\n\nuse super::super::utils::http_get;\nuse crate::{error::Result, Fees, Market, Precision, QuantityLimit};\n\nuse crypto_market_type::MarketType;\nuse serde::{Deserialize, Serialize};\nuse serde_json::Value;\n\n// https://www.gateio.pro/docs/apiv4/zh_CN/#contract\n#[derive(Clone, Serialize, Deserialize)]\n#[allow(non_snake_case)]\nstruct SwapMarket {\n    name: String,\n    #[serde(rename = \"type\")]\n    type_: String, // inverse, direct\n    quanto_multiplier: String,\n    leverage_min: String,\n    leverage_max: String,\n    maintenance_rate: String,\n    mark_type: String, // internal, index\n    maker_fee_rate: String,\n    taker_fee_rate: String,\n    order_price_round: String,\n    mark_price_round: String,\n    funding_rate: String,\n    order_size_min: f64,\n    order_size_max: f64,\n    in_delisting: bool,\n    #[serde(flatten)]\n    extra: HashMap<String, Value>,\n}\n\n// See https://www.gateio.pro/docs/apiv4/zh_CN/index.html#595cd9fe3c\nfn fetch_swap_markets_raw(settle: &str) -> Result<Vec<SwapMarket>> {\n    let txt = http_get(\n        format!(\"https://api.gateio.ws/api/v4/futures/{settle}/contracts\").as_str(),\n        None,\n    )?;\n    let markets = serde_json::from_str::<Vec<SwapMarket>>(&txt)?;\n    Ok(markets.into_iter().filter(|x| !x.in_delisting).collect::<Vec<SwapMarket>>())\n}\n\npub(super) fn fetch_inverse_swap_symbols() -> Result<Vec<String>> {\n    let symbols =\n        fetch_swap_markets_raw(\"btc\")?.into_iter().map(|m| m.name).collect::<Vec<String>>();\n    Ok(symbols)\n}\n\npub(super) fn fetch_linear_swap_symbols() -> Result<Vec<String>> {\n    let symbols =\n        fetch_swap_markets_raw(\"usdt\")?.into_iter().map(|m| m.name).collect::<Vec<String>>();\n    Ok(symbols)\n}\n\nfn to_market(raw_market: &SwapMarket) -> Market {\n    let pair = crypto_pair::normalize_pair(&raw_market.name, \"gate\").unwrap();\n    let (base, quote) = {\n        let v: Vec<&str> = pair.split('/').collect();\n        (v[0].to_string(), v[1].to_string())\n    };\n    let (base_id, quote_id) = {\n        let v: Vec<&str> = raw_market.name.split('_').collect();\n        (v[0].to_string(), v[1].to_string())\n    };\n    let market_type = if raw_market.name.ends_with(\"_USD\") {\n        MarketType::InverseSwap\n    } else if raw_market.name.ends_with(\"_USDT\") {\n        MarketType::LinearSwap\n    } else {\n        panic!(\"Failed to detect market type for {}\", raw_market.name);\n    };\n    let mut quanto_multiplier = raw_market.quanto_multiplier.parse::<f64>().unwrap();\n    if raw_market.name == \"BTC_USD\" {\n        assert_eq!(quanto_multiplier, 0.0);\n        quanto_multiplier = 1.0;\n    }\n    assert!(quanto_multiplier > 0.0);\n\n    Market {\n        exchange: \"gate\".to_string(),\n        market_type,\n        symbol: raw_market.name.to_string(),\n        base_id: base_id.clone(),\n        quote_id: quote_id.clone(),\n        settle_id: if market_type == MarketType::InverseSwap {\n            Some(base_id)\n        } else {\n            Some(quote_id)\n        },\n        base: base.clone(),\n        quote: quote.clone(),\n        settle: if market_type == MarketType::InverseSwap { Some(base) } else { Some(quote) },\n        active: !raw_market.in_delisting,\n        margin: true,\n        fees: Fees {\n            maker: raw_market.maker_fee_rate.parse::<f64>().unwrap(),\n            taker: raw_market.taker_fee_rate.parse::<f64>().unwrap(),\n        },\n        precision: Precision {\n            tick_size: raw_market.order_price_round.parse::<f64>().unwrap(),\n            lot_size: quanto_multiplier,\n        },\n        quantity_limit: Some(QuantityLimit {\n            min: Some(raw_market.order_size_min),\n            max: Some(raw_market.order_size_max),\n            notional_min: None,\n            notional_max: None,\n        }),\n        contract_value: Some(quanto_multiplier),\n        delivery_date: None,\n        info: serde_json::to_value(raw_market).unwrap().as_object().unwrap().clone(),\n    }\n}\n\npub(super) fn fetch_inverse_swap_markets() -> Result<Vec<Market>> {\n    let markets =\n        fetch_swap_markets_raw(\"btc\")?.into_iter().map(|m| to_market(&m)).collect::<Vec<Market>>();\n    Ok(markets)\n}\n\npub(super) fn fetch_linear_swap_markets() -> Result<Vec<Market>> {\n    let markets =\n        fetch_swap_markets_raw(\"usdt\")?.into_iter().map(|m| to_market(&m)).collect::<Vec<Market>>();\n    Ok(markets)\n}\n"
  },
  {
    "path": "crypto-markets/src/exchanges/gate/mod.rs",
    "content": "mod gate_future;\nmod gate_spot;\nmod gate_swap;\n\nuse crate::{error::Result, Market, MarketType};\n\npub(crate) fn fetch_symbols(market_type: MarketType) -> Result<Vec<String>> {\n    match market_type {\n        MarketType::Spot => gate_spot::fetch_spot_symbols(),\n        MarketType::InverseSwap => gate_swap::fetch_inverse_swap_symbols(),\n        MarketType::LinearSwap => gate_swap::fetch_linear_swap_symbols(),\n        MarketType::InverseFuture => gate_future::fetch_inverse_future_symbols(),\n        MarketType::LinearFuture => gate_future::fetch_linear_future_symbols(),\n        _ => panic!(\"Unsupported market_type: {market_type}\"),\n    }\n}\n\npub(crate) fn fetch_markets(market_type: MarketType) -> Result<Vec<Market>> {\n    match market_type {\n        MarketType::Spot => gate_spot::fetch_spot_markets(),\n        MarketType::InverseSwap => gate_swap::fetch_inverse_swap_markets(),\n        MarketType::LinearSwap => gate_swap::fetch_linear_swap_markets(),\n        MarketType::InverseFuture => gate_future::fetch_inverse_future_markets(),\n        MarketType::LinearFuture => gate_future::fetch_linear_future_markets(),\n        _ => panic!(\"Unsupported market_type: {market_type}\"),\n    }\n}\n"
  },
  {
    "path": "crypto-markets/src/exchanges/huobi/huobi_future.rs",
    "content": "use super::utils::huobi_http_get;\nuse crate::{\n    error::Result,\n    market::{Fees, Precision},\n    Market,\n};\n\nuse crypto_market_type::MarketType;\nuse serde::{Deserialize, Serialize};\nuse serde_json::Value;\nuse std::collections::HashMap;\n\n// https://github.com/ccxt/ccxt/issues/8074\n\n#[derive(Serialize, Deserialize)]\nstruct FutureMarket {\n    symbol: String,\n    contract_code: String,\n    contract_type: String,\n    contract_size: f64,\n    price_tick: f64,\n    delivery_date: String,\n    delivery_time: String,\n    create_date: String,\n    contract_status: i64,\n    settlement_time: String,\n    #[serde(flatten)]\n    extra: HashMap<String, Value>,\n}\n\n#[derive(Serialize, Deserialize)]\nstruct Response {\n    status: String,\n    data: Vec<FutureMarket>,\n    ts: i64,\n}\n\n// see <https://huobiapi.github.io/docs/dm/v1/en/#get-contract-info>\nfn fetch_future_markets_raw() -> Result<Vec<FutureMarket>> {\n    let txt = huobi_http_get(\"https://api.hbdm.com/api/v1/contract_contract_info\")?;\n    let resp = serde_json::from_str::<Response>(&txt)?;\n    let result: Vec<FutureMarket> =\n        resp.data.into_iter().filter(|m| m.contract_status == 1).collect();\n    Ok(result)\n}\n\npub(super) fn fetch_inverse_future_symbols() -> Result<Vec<String>> {\n    let symbols = fetch_future_markets_raw()?\n        .into_iter()\n        .map(|m| {\n            m.symbol.to_string()\n                + match m.contract_type.as_str() {\n                    \"this_week\" => \"_CW\",\n                    \"next_week\" => \"_NW\",\n                    \"quarter\" => \"_CQ\",\n                    \"next_quarter\" => \"_NQ\",\n                    contract_type => panic!(\"Unknown contract_type {contract_type}\"),\n                }\n        })\n        .collect::<Vec<String>>();\n    Ok(symbols)\n}\n\npub(super) fn fetch_inverse_future_markets() -> Result<Vec<Market>> {\n    let markets = fetch_future_markets_raw()?\n        .into_iter()\n        .map(|m| {\n            let symbol = m.symbol.to_string()\n                + match m.contract_type.as_str() {\n                    \"this_week\" => \"_CW\",\n                    \"next_week\" => \"_NW\",\n                    \"quarter\" => \"_CQ\",\n                    \"next_quarter\" => \"_NQ\",\n                    contract_type => panic!(\"Unknown contract_type {contract_type}\"),\n                };\n            let pair = crypto_pair::normalize_pair(&symbol, \"huobi\").unwrap();\n            let (base, quote) = {\n                let v: Vec<&str> = pair.split('/').collect();\n                (v[0].to_string(), v[1].to_string())\n            };\n            Market {\n                exchange: \"huobi\".to_string(),\n                market_type: MarketType::InverseFuture,\n                symbol,\n                base_id: m.symbol.to_string(),\n                quote_id: \"USD\".to_string(),\n                settle_id: Some(m.symbol.to_string()),\n                base: base.clone(),\n                quote,\n                settle: Some(base),\n                active: m.contract_status == 1,\n                margin: true,\n                // see https://futures.huobi.com/en-us/contract/fee_rate/\n                fees: Fees { maker: 0.0002, taker: 0.0004 },\n                precision: Precision { tick_size: m.price_tick, lot_size: 1.0 },\n                quantity_limit: None,\n                contract_value: Some(m.contract_size),\n                delivery_date: Some(m.delivery_time.parse::<u64>().unwrap()),\n                info: serde_json::to_value(&m).unwrap().as_object().unwrap().clone(),\n            }\n        })\n        .collect::<Vec<Market>>();\n    Ok(markets)\n}\n"
  },
  {
    "path": "crypto-markets/src/exchanges/huobi/huobi_inverse_swap.rs",
    "content": "use super::utils::huobi_http_get;\nuse crate::{\n    error::Result,\n    market::{Fees, Precision},\n    Market,\n};\n\nuse crypto_market_type::MarketType;\nuse serde::{Deserialize, Serialize};\nuse serde_json::Value;\nuse std::collections::HashMap;\n\n#[derive(Serialize, Deserialize)]\nstruct InverseSwapMarket {\n    symbol: String,\n    contract_code: String,\n    contract_size: f64,\n    price_tick: f64,\n    delivery_time: String,\n    create_date: String,\n    contract_status: i64,\n    settlement_date: String,\n    #[serde(flatten)]\n    extra: HashMap<String, Value>,\n}\n\n#[derive(Serialize, Deserialize)]\nstruct Response {\n    status: String,\n    data: Vec<InverseSwapMarket>,\n    ts: i64,\n}\n\n// see <https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#query-swap-info>\nfn fetch_inverse_swap_markets_raw() -> Result<Vec<InverseSwapMarket>> {\n    let txt = huobi_http_get(\"https://api.hbdm.com/swap-api/v1/swap_contract_info\")?;\n    let resp = serde_json::from_str::<Response>(&txt)?;\n    let result: Vec<InverseSwapMarket> =\n        resp.data.into_iter().filter(|m| m.contract_status == 1).collect();\n    Ok(result)\n}\n\npub(super) fn fetch_inverse_swap_symbols() -> Result<Vec<String>> {\n    let symbols = fetch_inverse_swap_markets_raw()?\n        .into_iter()\n        .map(|m| m.contract_code)\n        .collect::<Vec<String>>();\n    Ok(symbols)\n}\n\npub(super) fn fetch_inverse_swap_markets() -> Result<Vec<Market>> {\n    let markets = fetch_inverse_swap_markets_raw()?\n        .into_iter()\n        .map(|m| {\n            let info = serde_json::to_value(&m).unwrap().as_object().unwrap().clone();\n            let pair = crypto_pair::normalize_pair(&m.contract_code, \"huobi\").unwrap();\n            let (base, quote) = {\n                let v: Vec<&str> = pair.split('/').collect();\n                (v[0].to_string(), v[1].to_string())\n            };\n            Market {\n                exchange: \"huobi\".to_string(),\n                market_type: MarketType::InverseSwap,\n                symbol: m.contract_code,\n                base_id: m.symbol.to_string(),\n                quote_id: \"USD\".to_string(),\n                settle_id: Some(m.symbol.to_string()),\n                base: base.clone(),\n                quote,\n                settle: Some(base),\n                active: m.contract_status == 1,\n                margin: true,\n                // see https://futures.huobi.com/en-us/swap/fee_rate/\n                fees: Fees { maker: 0.0002, taker: 0.0005 },\n                precision: Precision { tick_size: m.price_tick, lot_size: 1.0 },\n                quantity_limit: None,\n                contract_value: Some(m.contract_size),\n                delivery_date: None,\n                info,\n            }\n        })\n        .collect::<Vec<Market>>();\n    Ok(markets)\n}\n"
  },
  {
    "path": "crypto-markets/src/exchanges/huobi/huobi_linear_swap.rs",
    "content": "use super::utils::huobi_http_get;\nuse crate::{\n    error::Result,\n    market::{Fees, Precision},\n    Market,\n};\n\nuse crypto_market_type::MarketType;\nuse serde::{Deserialize, Serialize};\nuse serde_json::Value;\nuse std::collections::HashMap;\n\n#[derive(Serialize, Deserialize)]\nstruct LinearSwapMarket {\n    symbol: String,\n    contract_code: String,\n    contract_size: f64,\n    price_tick: f64,\n    delivery_time: String,\n    create_date: String,\n    contract_status: i64,\n    settlement_date: String,\n    #[serde(flatten)]\n    extra: HashMap<String, Value>,\n}\n\n#[derive(Serialize, Deserialize)]\nstruct Response {\n    status: String,\n    data: Vec<LinearSwapMarket>,\n    ts: i64,\n}\n\n// see <https://huobiapi.github.io/docs/usdt_swap/v1/en/#general-query-swap-info>\nfn fetch_linear_swap_markets_raw() -> Result<Vec<LinearSwapMarket>> {\n    let txt = huobi_http_get(\"https://api.hbdm.com/linear-swap-api/v1/swap_contract_info\")?;\n    let resp = serde_json::from_str::<Response>(&txt)?;\n    let result: Vec<LinearSwapMarket> =\n        resp.data.into_iter().filter(|m| m.contract_status == 1).collect();\n    Ok(result)\n}\n\npub(super) fn fetch_linear_swap_symbols() -> Result<Vec<String>> {\n    let symbols = fetch_linear_swap_markets_raw()?\n        .into_iter()\n        .map(|m| m.contract_code)\n        .collect::<Vec<String>>();\n    Ok(symbols)\n}\n\npub(super) fn fetch_linear_swap_markets() -> Result<Vec<Market>> {\n    let markets = fetch_linear_swap_markets_raw()?\n        .into_iter()\n        .map(|m| {\n            let info = serde_json::to_value(&m).unwrap().as_object().unwrap().clone();\n            let pair = crypto_pair::normalize_pair(&m.contract_code, \"huobi\").unwrap();\n            let (base, quote) = {\n                let v: Vec<&str> = pair.split('/').collect();\n                (v[0].to_string(), v[1].to_string())\n            };\n            Market {\n                exchange: \"huobi\".to_string(),\n                market_type: MarketType::LinearSwap,\n                symbol: m.contract_code,\n                base_id: m.symbol.to_string(),\n                quote_id: \"USDT\".to_string(),\n                settle_id: Some(\"USDT\".to_string()),\n                base,\n                quote: quote.clone(),\n                settle: Some(quote),\n                active: m.contract_status == 1,\n                margin: true,\n                // see https://futures.huobi.com/en-us/linear_swap/fee_rate/\n                fees: Fees { maker: 0.0002, taker: 0.0004 },\n                precision: Precision { tick_size: m.price_tick, lot_size: 1.0 },\n                quantity_limit: None,\n                contract_value: Some(m.contract_size),\n                delivery_date: None,\n                info,\n            }\n        })\n        .collect::<Vec<Market>>();\n    Ok(markets)\n}\n"
  },
  {
    "path": "crypto-markets/src/exchanges/huobi/huobi_option.rs",
    "content": "use super::utils::huobi_http_get;\nuse crate::error::Result;\n\nuse serde::{Deserialize, Serialize};\nuse serde_json::Value;\nuse std::collections::HashMap;\n\n#[derive(Serialize, Deserialize)]\nstruct OptionMarket {\n    symbol: String,\n    contract_code: String,\n    contract_type: String,\n    contract_size: f64,\n    price_tick: f64,\n    delivery_date: String,\n    create_date: String,\n    contract_status: i64,\n    option_right_type: String,\n    exercise_price: f64,\n    delivery_asset: String,\n    quote_asset: String,\n    trade_partition: String,\n    #[serde(flatten)]\n    extra: HashMap<String, Value>,\n}\n\n#[derive(Serialize, Deserialize)]\nstruct Response {\n    status: String,\n    data: Vec<OptionMarket>,\n    ts: i64,\n}\n\n// see <https://huobiapi.github.io/docs/option/v1/en/#query-option-info>\nfn fetch_option_markets_raw() -> Result<Vec<OptionMarket>> {\n    let txt = huobi_http_get(\"https://api.hbdm.com/option-api/v1/option_contract_info\")?;\n    let resp = serde_json::from_str::<Response>(&txt)?;\n    let result: Vec<OptionMarket> =\n        resp.data.into_iter().filter(|m| m.contract_status == 1).collect();\n    Ok(result)\n}\n\npub(super) fn fetch_option_symbols() -> Result<Vec<String>> {\n    let symbols = fetch_option_markets_raw()?\n        .into_iter()\n        .filter(|m| m.contract_status == 1)\n        .map(|m| m.contract_code)\n        .collect::<Vec<String>>();\n    Ok(symbols)\n}\n"
  },
  {
    "path": "crypto-markets/src/exchanges/huobi/huobi_spot.rs",
    "content": "use super::utils::huobi_http_get;\nuse crate::{\n    error::Result,\n    market::{Fees, Precision, QuantityLimit},\n    Market,\n};\n\nuse crypto_market_type::MarketType;\nuse serde::{Deserialize, Serialize};\nuse serde_json::Value;\nuse std::collections::HashMap;\n\n#[derive(Serialize, Deserialize)]\n#[serde(rename_all = \"kebab-case\")]\nstruct SpotMarket {\n    base_currency: String,\n    quote_currency: String,\n    price_precision: f64,\n    amount_precision: f64,\n    symbol_partition: String,\n    symbol: String,\n    state: String,\n    value_precision: f64,\n    min_order_amt: f64,\n    max_order_amt: f64,\n    min_order_value: f64,\n    limit_order_min_order_amt: f64,\n    limit_order_max_order_amt: f64,\n    sell_market_min_order_amt: f64,\n    sell_market_max_order_amt: f64,\n    buy_market_max_order_value: f64,\n    api_trading: String,\n    #[serde(flatten)]\n    extra: HashMap<String, Value>,\n}\n\n#[derive(Serialize, Deserialize)]\nstruct Response {\n    status: String,\n    data: Vec<SpotMarket>,\n}\n\n// see <https://huobiapi.github.io/docs/spot/v1/en/#get-all-supported-trading-symbol>\nfn fetch_spot_markets_raw() -> Result<Vec<SpotMarket>> {\n    let txt = huobi_http_get(\"https://api.huobi.pro/v1/common/symbols\")?;\n    let resp = serde_json::from_str::<Response>(&txt)?;\n    let result: Vec<SpotMarket> = resp.data.into_iter().filter(|m| m.state == \"online\").collect();\n    Ok(result)\n}\n\npub(super) fn fetch_spot_symbols() -> Result<Vec<String>> {\n    let symbols = fetch_spot_markets_raw()?.into_iter().map(|m| m.symbol).collect::<Vec<String>>();\n    Ok(symbols)\n}\n\npub(super) fn fetch_spot_markets() -> Result<Vec<Market>> {\n    let markets = fetch_spot_markets_raw()?\n        .into_iter()\n        .map(|m| {\n            let info = serde_json::to_value(&m).unwrap().as_object().unwrap().clone();\n            let pair = crypto_pair::normalize_pair(&m.symbol, \"huobi\").unwrap();\n            let (base, quote) = {\n                let v: Vec<&str> = pair.split('/').collect();\n                (v[0].to_string(), v[1].to_string())\n            };\n            Market {\n                exchange: \"huobi\".to_string(),\n                market_type: MarketType::Spot,\n                symbol: m.symbol,\n                base_id: m.base_currency.to_string(),\n                quote_id: m.quote_currency.to_string(),\n                settle_id: None,\n                base,\n                quote,\n                settle: None,\n                active: m.state == \"online\",\n                margin: true,\n                // see https://www.huobi.com/en-us/fee/\n                fees: Fees { maker: 0.002, taker: 0.002 },\n                precision: Precision {\n                    tick_size: 1.0 / (10_i64.pow(m.price_precision as u32) as f64),\n                    lot_size: 1.0 / (10_i64.pow(m.amount_precision as u32) as f64),\n                },\n                quantity_limit: Some(QuantityLimit {\n                    min: Some(m.limit_order_min_order_amt),\n                    max: Some(m.limit_order_max_order_amt),\n                    notional_min: None,\n                    notional_max: None,\n                }),\n                contract_value: None,\n                delivery_date: None,\n                info,\n            }\n        })\n        .collect::<Vec<Market>>();\n    Ok(markets)\n}\n"
  },
  {
    "path": "crypto-markets/src/exchanges/huobi/mod.rs",
    "content": "mod utils;\n\npub(super) mod huobi_future;\npub(super) mod huobi_inverse_swap;\npub(super) mod huobi_linear_swap;\npub(super) mod huobi_option;\npub(super) mod huobi_spot;\n\nuse crate::{error::Result, Market, MarketType};\n\npub(crate) fn fetch_symbols(market_type: MarketType) -> Result<Vec<String>> {\n    match market_type {\n        MarketType::Spot => huobi_spot::fetch_spot_symbols(),\n        MarketType::InverseFuture => huobi_future::fetch_inverse_future_symbols(),\n        MarketType::InverseSwap => huobi_inverse_swap::fetch_inverse_swap_symbols(),\n        MarketType::LinearSwap => huobi_linear_swap::fetch_linear_swap_symbols(),\n        MarketType::EuropeanOption => huobi_option::fetch_option_symbols(),\n        _ => panic!(\"Unsupported market_type: {market_type}\"),\n    }\n}\n\npub(crate) fn fetch_markets(market_type: MarketType) -> Result<Vec<Market>> {\n    match market_type {\n        MarketType::Spot => huobi_spot::fetch_spot_markets(),\n        MarketType::InverseFuture => huobi_future::fetch_inverse_future_markets(),\n        MarketType::InverseSwap => huobi_inverse_swap::fetch_inverse_swap_markets(),\n        MarketType::LinearSwap => huobi_linear_swap::fetch_linear_swap_markets(),\n        MarketType::EuropeanOption => Ok(Vec::new()),\n        _ => panic!(\"Unsupported market_type: {market_type}\"),\n    }\n}\n"
  },
  {
    "path": "crypto-markets/src/exchanges/huobi/utils.rs",
    "content": "use super::super::utils::http_get;\nuse crate::error::{Error, Result};\n\nuse serde_json::Value;\nuse std::collections::HashMap;\n\nfn check_status_in_body(resp: String) -> Result<String> {\n    let obj = serde_json::from_str::<HashMap<String, Value>>(&resp);\n    if obj.is_err() {\n        return Ok(resp);\n    }\n\n    match obj.unwrap().get(\"status\") {\n        Some(status) => {\n            if status.as_str().unwrap() != \"ok\" {\n                Err(Error(resp))\n            } else {\n                Ok(resp)\n            }\n        }\n        None => Ok(resp),\n    }\n}\n\npub(super) fn huobi_http_get(url: &str) -> Result<String> {\n    let ret = http_get(url, None);\n    match ret {\n        Ok(resp) => check_status_in_body(resp),\n        Err(_) => ret,\n    }\n}\n"
  },
  {
    "path": "crypto-markets/src/exchanges/kraken/kraken_futures.rs",
    "content": "use std::collections::HashMap;\n\nuse super::super::utils::http_get;\nuse crate::{\n    error::{Error, Result},\n    Fees, Market, MarketType, Precision, QuantityLimit,\n};\n\nuse chrono::DateTime;\nuse serde::{Deserialize, Serialize};\nuse serde_json::Value;\n\n#[derive(Clone, Serialize, Deserialize)]\nstruct FuturesMarketPartial {\n    symbol: String,\n    #[serde(rename = \"type\")]\n    type_: String,\n    tradeable: bool,\n    #[serde(flatten)]\n    extra: HashMap<String, Value>,\n}\n\n#[derive(Clone, Serialize, Deserialize)]\n#[allow(non_snake_case)]\nstruct FuturesMarket {\n    symbol: String,\n    #[serde(rename = \"type\")]\n    type_: String,\n    tradeable: bool,\n    underlying: Option<String>,\n    lastTradingTime: Option<String>, // only applicable for futures\n    tickSize: f64,\n    contractSize: f64,\n    isin: Option<String>,\n    #[serde(flatten)]\n    extra: HashMap<String, Value>,\n}\n\n#[derive(Serialize, Deserialize)]\nstruct Response<T: Sized> {\n    result: String,\n    instruments: Vec<T>,\n}\n\nfn check_error_in_body(resp: String) -> Result<String> {\n    let obj = serde_json::from_str::<HashMap<String, Value>>(&resp);\n    if obj.is_err() {\n        return Err(Error(resp));\n    }\n    let obj = obj.unwrap();\n\n    if obj.get(\"result\").unwrap() != \"success\" {\n        return Err(Error(resp));\n    }\n    Ok(resp)\n}\n\npub(super) fn kraken_http_get(url: &str) -> Result<String> {\n    let ret = http_get(url, None);\n    match ret {\n        Ok(resp) => check_error_in_body(resp),\n        Err(_) => ret,\n    }\n}\n\n// see <https://www.kraken.com/features/api#get-tradable-pairs>\nfn fetch_futures_markets_raw() -> Result<Vec<FuturesMarket>> {\n    let txt = kraken_http_get(\"https://futures.kraken.com/derivatives/api/v3/instruments\")?;\n    let obj = serde_json::from_str::<Response<FuturesMarketPartial>>(&txt)?;\n    let markets = obj\n        .instruments\n        .into_iter()\n        .filter(|x| x.tradeable)\n        .map(|x| {\n            serde_json::from_str::<FuturesMarket>(serde_json::to_string(&x).unwrap().as_str())\n                .unwrap()\n        })\n        .collect::<Vec<FuturesMarket>>();\n    Ok(markets)\n}\n\npub(super) fn fetch_inverse_future_symbols() -> Result<Vec<String>> {\n    let symbols = fetch_futures_markets_raw()?\n        .into_iter()\n        .filter(|x| x.symbol.starts_with(\"fi_\"))\n        .map(|m| m.symbol.to_uppercase())\n        .collect::<Vec<String>>();\n    Ok(symbols)\n}\n\npub(super) fn fetch_inverse_swap_symbols() -> Result<Vec<String>> {\n    let symbols = fetch_futures_markets_raw()?\n        .into_iter()\n        .filter(|x| x.symbol.starts_with(\"pi_\"))\n        .map(|m| m.symbol.to_uppercase())\n        .collect::<Vec<String>>();\n    Ok(symbols)\n}\n\npub(super) fn fetch_inverse_future_markets() -> Result<Vec<Market>> {\n    let markets = fetch_futures_markets()?\n        .into_iter()\n        .filter(|x| x.market_type == MarketType::InverseFuture)\n        .collect::<Vec<Market>>();\n    Ok(markets)\n}\n\npub(super) fn fetch_inverse_swap_markets() -> Result<Vec<Market>> {\n    let markets = fetch_futures_markets()?\n        .into_iter()\n        .filter(|x| x.market_type == MarketType::InverseSwap)\n        .collect::<Vec<Market>>();\n    Ok(markets)\n}\n\nfn fetch_futures_markets() -> Result<Vec<Market>> {\n    let markets = fetch_futures_markets_raw()?\n        .into_iter()\n        .filter(|m| m.symbol.starts_with(\"pi_\") || m.symbol.starts_with(\"fi_\")) // TODO: Multi-Collateral, e.g., pf_xbtusd\n        .map(|m| {\n            let info = serde_json::to_value(&m).unwrap().as_object().unwrap().clone();\n            let pair = crypto_pair::normalize_pair(&m.symbol, \"kraken\").unwrap();\n            let (base, quote) = {\n                let v: Vec<&str> = pair.split('/').collect();\n                (v[0].to_string(), v[1].to_string())\n            };\n            let (base_id, quote_id) = {\n                let pos = m.symbol.find(\"usd\").unwrap();\n                (m.symbol[3..pos].to_string(), \"usd\".to_string())\n            };\n            Market {\n                exchange: \"kraken\".to_string(),\n                market_type: if m.symbol.starts_with(\"fi_\") {\n                    MarketType::InverseFuture\n                } else if m.symbol.starts_with(\"pi_\") {\n                    MarketType::InverseSwap\n                } else {\n                    MarketType::Unknown\n                },\n                symbol: m.symbol,\n                base_id: base_id.clone(),\n                quote_id,\n                settle_id: Some(base_id),\n                base: base.clone(),\n                quote,\n                settle: Some(base),\n                active: m.tradeable,\n                margin: true,\n                // see https://futures.kraken.com/derivatives/api/v3/feeschedules\n                fees: Fees { maker: 0.0002, taker: 0.0005 },\n                precision: Precision { tick_size: m.tickSize, lot_size: 1.0 },\n                quantity_limit: Some(QuantityLimit {\n                    min: Some(1.0),\n                    max: None,\n                    notional_min: None,\n                    notional_max: None,\n                }),\n                contract_value: Some(m.contractSize),\n                delivery_date: m\n                    .lastTradingTime\n                    .map(|x| DateTime::parse_from_rfc3339(&x).unwrap().timestamp_millis() as u64),\n                info,\n            }\n        })\n        .collect::<Vec<Market>>();\n    Ok(markets)\n}\n"
  },
  {
    "path": "crypto-markets/src/exchanges/kraken/kraken_spot.rs",
    "content": "use std::collections::HashMap;\n\nuse super::super::utils::http_get;\nuse crate::{\n    error::{Error, Result},\n    Fees, Market, MarketType, Precision, QuantityLimit,\n};\n\nuse serde::{Deserialize, Serialize};\nuse serde_json::Value;\n\n#[derive(Clone, Serialize, Deserialize)]\nstruct SpotMarket {\n    altname: String,\n    wsname: Option<String>,\n    aclass_base: String,\n    base: String,\n    aclass_quote: String,\n    quote: String,\n    lot: String,\n    pair_decimals: i64,\n    lot_decimals: i64,\n    lot_multiplier: i64,\n    fees: Vec<Vec<f64>>,\n    fees_maker: Vec<Vec<f64>>,\n    fee_volume_currency: String,\n    margin_call: i64,\n    margin_stop: i64,\n    ordermin: String,\n}\n\n#[derive(Serialize, Deserialize)]\nstruct Response {\n    result: HashMap<String, SpotMarket>,\n}\n\nfn check_error_in_body(resp: String) -> Result<String> {\n    let obj = serde_json::from_str::<HashMap<String, Value>>(&resp);\n    if obj.is_err() {\n        return Err(Error(resp));\n    }\n\n    match obj.unwrap().get(\"error\") {\n        Some(err) => {\n            let arr = err.as_array().unwrap();\n            if arr.is_empty() { Ok(resp) } else { Err(Error(resp)) }\n        }\n        None => Ok(resp),\n    }\n}\n\npub(super) fn kraken_http_get(url: &str) -> Result<String> {\n    let ret = http_get(url, None);\n    match ret {\n        Ok(resp) => check_error_in_body(resp),\n        Err(_) => ret,\n    }\n}\n\n// see <https://docs.kraken.com/rest/#operation/getTradableAssetPairs>\nfn fetch_spot_markets_raw() -> Result<Vec<SpotMarket>> {\n    let txt = kraken_http_get(\"https://api.kraken.com/0/public/AssetPairs\")?;\n    let obj = serde_json::from_str::<HashMap<String, Value>>(&txt)?;\n    let markets = obj\n        .get(\"result\")\n        .unwrap()\n        .as_object()\n        .unwrap()\n        .values()\n        .filter(|x| x.as_object().unwrap().contains_key(\"wsname\"))\n        .map(|x| serde_json::from_value::<SpotMarket>(x.clone()).unwrap())\n        .collect::<Vec<SpotMarket>>();\n    Ok(markets)\n}\n\npub(super) fn fetch_spot_symbols() -> Result<Vec<String>> {\n    let symbols =\n        fetch_spot_markets_raw()?.into_iter().filter_map(|m| m.wsname).collect::<Vec<String>>();\n    Ok(symbols)\n}\n\npub(super) fn fetch_spot_markets() -> Result<Vec<Market>> {\n    let markets = fetch_spot_markets_raw()?\n        .into_iter()\n        .map(|m| {\n            let info = serde_json::to_value(&m).unwrap().as_object().unwrap().clone();\n            let symbol = m.wsname.unwrap();\n            let pair = crypto_pair::normalize_pair(&symbol, \"kraken\").unwrap();\n            let (base, quote) = {\n                let v: Vec<&str> = pair.split('/').collect();\n                (v[0].to_string(), v[1].to_string())\n            };\n            Market {\n                exchange: \"kraken\".to_string(),\n                market_type: MarketType::Spot,\n                symbol,\n                base_id: m.base,\n                quote_id: m.quote,\n                settle_id: None,\n                base,\n                quote,\n                settle: None,\n                active: true,\n                margin: false,\n                // see https://support.kraken.com/hc/en-us/articles/360000526126-What-are-Maker-and-Taker-fees-\n                fees: Fees { maker: 0.0016, taker: 0.0026 },\n                precision: Precision {\n                    tick_size: 1.0 / (10_i64.pow(m.pair_decimals as u32) as f64),\n                    lot_size: 1.0 / (10_i64.pow(m.lot_decimals as u32) as f64),\n                },\n                quantity_limit: Some(QuantityLimit {\n                    min: m.ordermin.parse::<f64>().ok(),\n                    max: None,\n                    notional_min: None,\n                    notional_max: None,\n                }),\n                contract_value: None,\n                delivery_date: None,\n                info,\n            }\n        })\n        .collect::<Vec<Market>>();\n    Ok(markets)\n}\n"
  },
  {
    "path": "crypto-markets/src/exchanges/kraken/mod.rs",
    "content": "mod kraken_futures;\nmod kraken_spot;\n\nuse crate::{error::Result, Market, MarketType};\n\npub(crate) fn fetch_symbols(market_type: MarketType) -> Result<Vec<String>> {\n    match market_type {\n        MarketType::Spot => kraken_spot::fetch_spot_symbols(),\n        MarketType::InverseFuture => kraken_futures::fetch_inverse_future_symbols(),\n        MarketType::InverseSwap => kraken_futures::fetch_inverse_swap_symbols(),\n        _ => panic!(\"Unsupported market_type: {market_type}\"),\n    }\n}\n\npub(crate) fn fetch_markets(market_type: MarketType) -> Result<Vec<Market>> {\n    match market_type {\n        MarketType::Spot => kraken_spot::fetch_spot_markets(),\n        MarketType::InverseFuture => kraken_futures::fetch_inverse_future_markets(),\n        MarketType::InverseSwap => kraken_futures::fetch_inverse_swap_markets(),\n        _ => panic!(\"Unsupported market_type: {market_type}\"),\n    }\n}\n"
  },
  {
    "path": "crypto-markets/src/exchanges/kucoin/kucoin_spot.rs",
    "content": "use std::collections::HashMap;\n\nuse super::super::utils::http_get;\nuse crate::{\n    error::{Error, Result},\n    Fees, Market, Precision, QuantityLimit,\n};\n\nuse crypto_market_type::MarketType;\nuse serde::{Deserialize, Serialize};\nuse serde_json::Value;\n\n#[derive(Clone, Serialize, Deserialize)]\n#[allow(non_snake_case)]\nstruct SpotMarket {\n    symbol: String,\n    name: String,\n    baseCurrency: String,\n    quoteCurrency: String,\n    feeCurrency: String,\n    market: String,\n    baseMinSize: String,\n    quoteMinSize: String,\n    baseMaxSize: String,\n    quoteMaxSize: String,\n    baseIncrement: String,\n    quoteIncrement: String,\n    priceIncrement: String,\n    priceLimitRate: String,\n    isMarginEnabled: bool,\n    enableTrading: bool,\n    #[serde(flatten)]\n    extra: HashMap<String, Value>,\n}\n\n#[derive(Serialize, Deserialize)]\nstruct Response {\n    code: String,\n    data: Vec<SpotMarket>,\n}\n\n// See https://docs.kucoin.com/#get-symbols-list\nfn fetch_spot_markets_raw() -> Result<Vec<SpotMarket>> {\n    let txt = http_get(\"https://api.kucoin.com/api/v1/symbols\", None)?;\n    let resp = serde_json::from_str::<Response>(&txt)?;\n    if resp.code != \"200000\" {\n        Err(Error(txt))\n    } else {\n        let markets =\n            resp.data.into_iter().filter(|x| x.enableTrading).collect::<Vec<SpotMarket>>();\n        Ok(markets)\n    }\n}\n\npub(super) fn fetch_spot_symbols() -> Result<Vec<String>> {\n    let markets = fetch_spot_markets_raw()?;\n    let symbols: Vec<String> = markets.into_iter().map(|m| m.symbol).collect();\n    Ok(symbols)\n}\n\npub(super) fn fetch_spot_markets() -> Result<Vec<Market>> {\n    let markets: Vec<Market> = fetch_spot_markets_raw()?\n        .into_iter()\n        .map(|m| {\n            let info = serde_json::to_value(&m).unwrap().as_object().unwrap().clone();\n            let pair = crypto_pair::normalize_pair(&m.symbol, \"kucoin\").unwrap();\n            let (base, quote) = {\n                let v: Vec<&str> = pair.split('/').collect();\n                (v[0].to_string(), v[1].to_string())\n            };\n            Market {\n                exchange: \"kucoin\".to_string(),\n                market_type: MarketType::Spot,\n                symbol: m.symbol,\n                base_id: m.baseCurrency,\n                quote_id: m.quoteCurrency,\n                settle_id: None,\n                base,\n                quote,\n                settle: None,\n                active: m.enableTrading,\n                margin: m.isMarginEnabled,\n                // see https://www.bitstamp.net/fee-schedule/\n                fees: Fees { maker: 0.005, taker: 0.005 },\n                precision: Precision {\n                    tick_size: m.priceIncrement.parse::<f64>().unwrap(),\n                    lot_size: m.baseIncrement.parse::<f64>().unwrap(),\n                },\n                quantity_limit: Some(QuantityLimit {\n                    min: m.baseMinSize.parse::<f64>().ok(),\n                    max: Some(m.baseMaxSize.parse::<f64>().unwrap()),\n                    notional_min: None,\n                    notional_max: None,\n                }),\n                contract_value: None,\n                delivery_date: None,\n                info,\n            }\n        })\n        .collect();\n    Ok(markets)\n}\n"
  },
  {
    "path": "crypto-markets/src/exchanges/kucoin/kucoin_swap.rs",
    "content": "use std::collections::HashMap;\n\nuse super::super::utils::http_get;\nuse crate::{\n    error::{Error, Result},\n    Fees, Market, Precision,\n};\n\nuse crypto_market_type::MarketType;\nuse serde::{Deserialize, Serialize};\nuse serde_json::Value;\n\n#[derive(Clone, Serialize, Deserialize)]\n#[allow(non_snake_case)]\nstruct SwapMarket {\n    symbol: String,\n    rootSymbol: String,\n    #[serde(rename = \"type\")]\n    type_: String,\n    expireDate: Option<u64>,\n    baseCurrency: String,\n    quoteCurrency: String,\n    settleCurrency: String,\n    maxOrderQty: i64,\n    maxPrice: f64,\n    lotSize: f64,\n    tickSize: f64,\n    indexPriceTickSize: f64,\n    multiplier: f64,\n    initialMargin: f64,\n    maintainMargin: f64,\n    maxRiskLimit: i64,\n    minRiskLimit: i64,\n    riskStep: i64,\n    makerFeeRate: f64,\n    takerFeeRate: f64,\n    takerFixFee: f64,\n    makerFixFee: f64,\n    isDeleverage: bool,\n    isQuanto: bool,\n    isInverse: bool,\n    markMethod: String,\n    fairMethod: Option<String>,\n    status: String,\n    fundingFeeRate: Option<f64>,\n    predictedFundingFeeRate: Option<f64>,\n    openInterest: String,\n    #[serde(flatten)]\n    extra: HashMap<String, Value>,\n}\n\n#[derive(Serialize, Deserialize)]\nstruct Response {\n    code: String,\n    data: Vec<SwapMarket>,\n}\n\n// See https://docs.kucoin.com/#get-symbols-list\nfn fetch_swap_markets_raw() -> Result<Vec<SwapMarket>> {\n    let txt = http_get(\"https://api-futures.kucoin.com/api/v1/contracts/active\", None)?;\n    let resp = serde_json::from_str::<Response>(&txt)?;\n    if resp.code != \"200000\" {\n        Err(Error(txt))\n    } else {\n        let markets =\n            resp.data.into_iter().filter(|x| x.status == \"Open\").collect::<Vec<SwapMarket>>();\n        Ok(markets)\n    }\n}\n\npub(super) fn fetch_inverse_swap_symbols() -> Result<Vec<String>> {\n    let markets = fetch_swap_markets_raw()?;\n    let symbols: Vec<String> = markets\n        .into_iter()\n        .filter(|x| x.isInverse && x.type_ == \"FFWCSX\")\n        .map(|m| m.symbol)\n        .collect();\n    Ok(symbols)\n}\n\npub(super) fn fetch_linear_swap_symbols() -> Result<Vec<String>> {\n    let markets = fetch_swap_markets_raw()?;\n    let symbols: Vec<String> = markets\n        .into_iter()\n        .filter(|x| !x.isInverse && x.type_ == \"FFWCSX\")\n        .map(|m| m.symbol)\n        .collect();\n    Ok(symbols)\n}\n\npub(super) fn fetch_inverse_future_symbols() -> Result<Vec<String>> {\n    let markets = fetch_swap_markets_raw()?;\n    let symbols: Vec<String> = markets\n        .into_iter()\n        .filter(|x| x.isInverse && x.type_ == \"FFICSX\")\n        .map(|m| m.symbol)\n        .collect();\n    Ok(symbols)\n}\n\nfn to_market(raw_market: &SwapMarket) -> Market {\n    let pair = crypto_pair::normalize_pair(&raw_market.symbol, \"kucoin\").unwrap();\n    let (base, quote) = {\n        let v: Vec<&str> = pair.split('/').collect();\n        (v[0].to_string(), v[1].to_string())\n    };\n    let market_type = if raw_market.isInverse && raw_market.type_ == \"FFWCSX\" {\n        MarketType::InverseSwap\n    } else if !raw_market.isInverse && raw_market.type_ == \"FFWCSX\" {\n        MarketType::LinearSwap\n    } else if raw_market.isInverse && raw_market.type_ == \"FFICSX\" {\n        MarketType::InverseFuture\n    } else {\n        panic!(\n            \"Failed to detect market_type {}\",\n            serde_json::to_string_pretty(raw_market).unwrap()\n        );\n    };\n\n    Market {\n        exchange: \"kucoin\".to_string(),\n        market_type,\n        symbol: raw_market.symbol.to_string(),\n        base_id: raw_market.baseCurrency.to_string(),\n        quote_id: raw_market.quoteCurrency.to_string(),\n        settle_id: if raw_market.isInverse {\n            Some(raw_market.baseCurrency.to_string())\n        } else {\n            Some(raw_market.quoteCurrency.to_string())\n        },\n        base: base.clone(),\n        quote: quote.clone(),\n        settle: if raw_market.isInverse { Some(base) } else { Some(quote) },\n        active: raw_market.status == \"Open\",\n        margin: true,\n        fees: Fees { maker: raw_market.makerFeeRate, taker: raw_market.takerFeeRate },\n        precision: Precision { tick_size: raw_market.tickSize, lot_size: raw_market.lotSize },\n        quantity_limit: None,\n        contract_value: Some(raw_market.multiplier.abs()),\n        delivery_date: raw_market.expireDate,\n        info: serde_json::to_value(raw_market).unwrap().as_object().unwrap().clone(),\n    }\n}\n\npub(super) fn fetch_inverse_swap_markets() -> Result<Vec<Market>> {\n    let markets: Vec<Market> = fetch_swap_markets_raw()?\n        .into_iter()\n        .filter(|x| x.isInverse && x.type_ == \"FFWCSX\")\n        .map(|m| to_market(&m))\n        .collect();\n    Ok(markets)\n}\n\npub(super) fn fetch_linear_swap_markets() -> Result<Vec<Market>> {\n    let markets: Vec<Market> = fetch_swap_markets_raw()?\n        .into_iter()\n        .filter(|x| !x.isInverse && x.type_ == \"FFWCSX\")\n        .map(|m| to_market(&m))\n        .collect();\n    Ok(markets)\n}\n\npub(super) fn fetch_inverse_future_markets() -> Result<Vec<Market>> {\n    let markets: Vec<Market> = fetch_swap_markets_raw()?\n        .into_iter()\n        .filter(|x| x.isInverse && x.type_ == \"FFICSX\")\n        .map(|m| to_market(&m))\n        .collect();\n    Ok(markets)\n}\n"
  },
  {
    "path": "crypto-markets/src/exchanges/kucoin/mod.rs",
    "content": "mod kucoin_spot;\nmod kucoin_swap;\n\nuse crate::{error::Result, Market, MarketType};\n\npub(crate) fn fetch_symbols(market_type: MarketType) -> Result<Vec<String>> {\n    match market_type {\n        MarketType::Spot => kucoin_spot::fetch_spot_symbols(),\n        MarketType::InverseSwap => kucoin_swap::fetch_inverse_swap_symbols(),\n        MarketType::LinearSwap => kucoin_swap::fetch_linear_swap_symbols(),\n        MarketType::InverseFuture => kucoin_swap::fetch_inverse_future_symbols(),\n        _ => panic!(\"Unsupported market_type: {market_type}\"),\n    }\n}\n\npub(crate) fn fetch_markets(market_type: MarketType) -> Result<Vec<Market>> {\n    match market_type {\n        MarketType::Spot => kucoin_spot::fetch_spot_markets(),\n        MarketType::InverseSwap => kucoin_swap::fetch_inverse_swap_markets(),\n        MarketType::LinearSwap => kucoin_swap::fetch_linear_swap_markets(),\n        MarketType::InverseFuture => kucoin_swap::fetch_inverse_future_markets(),\n        _ => panic!(\"Unsupported market_type: {market_type}\"),\n    }\n}\n"
  },
  {
    "path": "crypto-markets/src/exchanges/mexc/mexc_spot.rs",
    "content": "use super::utils::mexc_http_get;\nuse crate::{error::Result, Fees, Market, Precision, QuantityLimit};\n\nuse crypto_market_type::MarketType;\nuse serde::{Deserialize, Serialize};\nuse serde_json::Value;\nuse std::collections::HashMap;\n\n#[derive(Serialize, Deserialize)]\nstruct SpotMarket {\n    symbol: String,\n    state: String,\n    price_scale: u32,\n    quantity_scale: u32,\n    min_amount: String,\n    max_amount: String,\n    maker_fee_rate: String,\n    taker_fee_rate: String,\n    limited: bool,\n    #[serde(flatten)]\n    extra: HashMap<String, Value>,\n}\n\n#[derive(Serialize, Deserialize)]\nstruct Response {\n    code: i64,\n    data: Vec<SpotMarket>,\n}\n\n// see <https://mxcdevelop.github.io/APIDoc/open.api.v2.en.html#all-symbols>\nfn fetch_spot_markets_raw() -> Result<Vec<SpotMarket>> {\n    let txt = mexc_http_get(\"https://www.mexc.com/open/api/v2/market/symbols\")?;\n    let resp = serde_json::from_str::<Response>(&txt)?;\n    Ok(resp.data.into_iter().filter(|m| m.state == \"ENABLED\" && !m.limited).collect())\n}\n\npub(super) fn fetch_spot_symbols() -> Result<Vec<String>> {\n    let symbols = fetch_spot_markets_raw()?.into_iter().map(|m| m.symbol).collect::<Vec<String>>();\n    Ok(symbols)\n}\n\npub(super) fn fetch_spot_markets() -> Result<Vec<Market>> {\n    let markets = fetch_spot_markets_raw()?\n        .into_iter()\n        .map(|m| {\n            let info = serde_json::to_value(&m).unwrap().as_object().unwrap().clone();\n            let pair = crypto_pair::normalize_pair(&m.symbol, super::EXCHANGE_NAME).unwrap();\n            let (base, quote) = {\n                let v: Vec<&str> = pair.split('/').collect();\n                (v[0].to_string(), v[1].to_string())\n            };\n            let (base_id, quote_id) = {\n                let v: Vec<&str> = m.symbol.split('_').collect();\n                (v[0].to_string(), v[1].to_string())\n            };\n            Market {\n                exchange: super::EXCHANGE_NAME.to_string(),\n                market_type: MarketType::Spot,\n                symbol: m.symbol,\n                base_id,\n                quote_id,\n                settle_id: None,\n                base,\n                quote,\n                settle: None,\n                active: m.state == \"ENABLED\" && !m.limited,\n                margin: false,\n                fees: Fees {\n                    maker: m.maker_fee_rate.parse::<f64>().unwrap(),\n                    taker: m.taker_fee_rate.parse::<f64>().unwrap(),\n                },\n                precision: Precision {\n                    tick_size: 1.0 / (10_i64.pow(m.price_scale) as f64),\n                    lot_size: 1.0 / (10_i64.pow(m.quantity_scale) as f64),\n                },\n                quantity_limit: Some(QuantityLimit {\n                    min: m.min_amount.parse::<f64>().ok(),\n                    max: Some(m.max_amount.parse::<f64>().unwrap()),\n                    notional_min: None,\n                    notional_max: None,\n                }),\n                contract_value: None,\n                delivery_date: None,\n                info,\n            }\n        })\n        .collect::<Vec<Market>>();\n    Ok(markets)\n}\n"
  },
  {
    "path": "crypto-markets/src/exchanges/mexc/mexc_swap.rs",
    "content": "use super::utils::mexc_http_get;\nuse crate::{error::Result, Fees, Market, Precision, QuantityLimit};\n\nuse crypto_market_type::MarketType;\nuse serde::{Deserialize, Serialize};\nuse serde_json::Value;\nuse std::collections::HashMap;\n\n#[derive(Serialize, Deserialize)]\n#[allow(non_snake_case)]\nstruct SwapMarket {\n    symbol: String,\n    displayName: String,\n    displayNameEn: String,\n    positionOpenType: i64,\n    baseCoin: String,\n    quoteCoin: String,\n    settleCoin: String,\n    contractSize: f64,\n    minLeverage: i64,\n    maxLeverage: i64,\n    priceScale: i64,\n    volScale: i64,\n    amountScale: i64,\n    priceUnit: f64,\n    volUnit: i64,\n    minVol: i64,\n    maxVol: i64,\n    bidLimitPriceRate: f64,\n    askLimitPriceRate: f64,\n    takerFeeRate: f64,\n    makerFeeRate: f64,\n    maintenanceMarginRate: f64,\n    initialMarginRate: f64,\n    state: i64,\n    isNew: bool,\n    isHot: bool,\n    isHidden: bool,\n    #[serde(flatten)]\n    extra: HashMap<String, Value>,\n}\n\n#[derive(Serialize, Deserialize)]\nstruct Response {\n    success: bool,\n    code: i64,\n    data: Vec<SwapMarket>,\n}\n\n// see <https://github.com/mxcdevelop/APIDoc/blob/master/contract/contract-api.md#contract-interface-public>\nfn fetch_swap_markets_raw() -> Result<Vec<SwapMarket>> {\n    let txt = mexc_http_get(\"https://contract.mexc.com/api/v1/contract/detail\")?;\n    let resp = serde_json::from_str::<Response>(&txt)?;\n    Ok(resp.data.into_iter().filter(|m| m.state == 0 && !m.isHidden).collect())\n}\n\npub(super) fn fetch_linear_swap_symbols() -> Result<Vec<String>> {\n    let symbols = fetch_swap_markets_raw()?\n        .into_iter()\n        .filter(|m| m.settleCoin == m.quoteCoin)\n        .map(|m| m.symbol)\n        .collect::<Vec<String>>();\n    Ok(symbols)\n}\n\npub(super) fn fetch_inverse_swap_symbols() -> Result<Vec<String>> {\n    let symbols = fetch_swap_markets_raw()?\n        .into_iter()\n        .filter(|m| m.settleCoin == m.baseCoin)\n        .map(|m| m.symbol)\n        .collect::<Vec<String>>();\n    Ok(symbols)\n}\n\nfn to_market(raw_market: &SwapMarket) -> Market {\n    let pair = crypto_pair::normalize_pair(&raw_market.symbol, super::EXCHANGE_NAME).unwrap();\n    let (base, quote) = {\n        let v: Vec<&str> = pair.split('/').collect();\n        (v[0].to_string(), v[1].to_string())\n    };\n    let market_type = if raw_market.settleCoin == raw_market.quoteCoin {\n        MarketType::LinearSwap\n    } else if raw_market.settleCoin == raw_market.baseCoin {\n        MarketType::InverseSwap\n    } else {\n        panic!(\"unexpected market type\");\n    };\n\n    Market {\n        exchange: super::EXCHANGE_NAME.to_string(),\n        market_type,\n        symbol: raw_market.symbol.to_string(),\n        base_id: raw_market.baseCoin.to_string(),\n        quote_id: raw_market.quoteCoin.to_string(),\n        settle_id: Some(raw_market.settleCoin.to_string()),\n        base,\n        quote,\n        settle: Some(raw_market.settleCoin.to_string()),\n        active: raw_market.state == 0 && !raw_market.isHidden,\n        margin: true,\n        fees: Fees { maker: raw_market.makerFeeRate, taker: raw_market.takerFeeRate },\n        precision: Precision {\n            tick_size: raw_market.priceUnit,\n            lot_size: raw_market.volUnit as f64,\n        },\n        quantity_limit: Some(QuantityLimit {\n            min: Some(raw_market.minVol as f64),\n            max: Some(raw_market.maxVol as f64),\n            notional_min: None,\n            notional_max: None,\n        }),\n        contract_value: Some(raw_market.contractSize),\n        delivery_date: None,\n        info: serde_json::to_value(raw_market).unwrap().as_object().unwrap().clone(),\n    }\n}\n\npub(super) fn fetch_linear_swap_markets() -> Result<Vec<Market>> {\n    let markets = fetch_swap_markets_raw()?\n        .into_iter()\n        .filter(|m| m.settleCoin == m.quoteCoin)\n        .map(|m| to_market(&m))\n        .collect::<Vec<Market>>();\n    Ok(markets)\n}\n\npub(super) fn fetch_inverse_swap_markets() -> Result<Vec<Market>> {\n    let markets = fetch_swap_markets_raw()?\n        .into_iter()\n        .filter(|m| m.settleCoin == m.baseCoin)\n        .map(|m| to_market(&m))\n        .collect::<Vec<Market>>();\n    Ok(markets)\n}\n"
  },
  {
    "path": "crypto-markets/src/exchanges/mexc/mod.rs",
    "content": "mod utils;\n\npub(super) mod mexc_spot;\npub(super) mod mexc_swap;\n\nuse crate::{error::Result, Market, MarketType};\n\npub(super) const EXCHANGE_NAME: &str = \"mexc\";\n\npub(crate) fn fetch_symbols(market_type: MarketType) -> Result<Vec<String>> {\n    match market_type {\n        MarketType::Spot => mexc_spot::fetch_spot_symbols(),\n        MarketType::InverseSwap => mexc_swap::fetch_inverse_swap_symbols(),\n        MarketType::LinearSwap => mexc_swap::fetch_linear_swap_symbols(),\n        _ => panic!(\"Unsupported market_type: {market_type}\"),\n    }\n}\n\npub(crate) fn fetch_markets(market_type: MarketType) -> Result<Vec<Market>> {\n    match market_type {\n        MarketType::Spot => mexc_spot::fetch_spot_markets(),\n        MarketType::InverseSwap => mexc_swap::fetch_inverse_swap_markets(),\n        MarketType::LinearSwap => mexc_swap::fetch_linear_swap_markets(),\n        _ => panic!(\"Unsupported market_type: {market_type}\"),\n    }\n}\n"
  },
  {
    "path": "crypto-markets/src/exchanges/mexc/utils.rs",
    "content": "use super::super::utils::http_get;\nuse crate::error::{Error, Result};\n\nuse serde_json::Value;\nuse std::collections::HashMap;\n\nfn check_code_in_body(resp: String) -> Result<String> {\n    let obj = serde_json::from_str::<HashMap<String, Value>>(&resp);\n    if obj.is_err() {\n        return Ok(resp);\n    }\n\n    match obj.unwrap().get(\"code\") {\n        Some(code) => {\n            let code_int = code.as_i64().unwrap();\n            if code_int == 0 || code_int == 200 { Ok(resp) } else { Err(Error(resp)) }\n        }\n        None => Ok(resp),\n    }\n}\n\npub(super) fn mexc_http_get(url: &str) -> Result<String> {\n    let ret = http_get(url, None);\n    match ret {\n        Ok(resp) => check_code_in_body(resp),\n        Err(_) => ret,\n    }\n}\n"
  },
  {
    "path": "crypto-markets/src/exchanges/mod.rs",
    "content": "#[macro_use]\nmod utils;\n\npub(super) mod binance;\npub(super) mod bitfinex;\npub(super) mod bitget;\npub(super) mod bithumb;\npub(super) mod bitmex;\npub(super) mod bitstamp;\npub(super) mod bitz;\npub(super) mod bybit;\npub(super) mod coinbase_pro;\npub(super) mod deribit;\npub(super) mod dydx;\npub(super) mod ftx;\npub(super) mod gate;\npub(super) mod huobi;\npub(super) mod kraken;\npub(super) mod kucoin;\npub(super) mod mexc;\npub(super) mod okx;\npub(super) mod zb;\npub(super) mod zbg;\n"
  },
  {
    "path": "crypto-markets/src/exchanges/okx.rs",
    "content": "use std::collections::HashMap;\n\nuse super::utils::http_get;\nuse crate::{\n    error::Result,\n    market::{Fees, Precision, QuantityLimit},\n    Market,\n};\n\n// use chrono::DateTime;\nuse crypto_market_type::MarketType;\nuse serde::{Deserialize, Serialize};\nuse serde_json::Value;\n\npub(crate) fn fetch_symbols(market_type: MarketType) -> Result<Vec<String>> {\n    match market_type {\n        MarketType::Spot => fetch_spot_symbols(),\n        MarketType::InverseFuture => fetch_inverse_future_symbols(),\n        MarketType::LinearFuture => fetch_linear_future_symbols(),\n        MarketType::InverseSwap => fetch_inverse_swap_symbols(),\n        MarketType::LinearSwap => fetch_linear_swap_symbols(),\n        MarketType::EuropeanOption => fetch_option_symbols(),\n        _ => panic!(\"Unsupported market_type: {market_type}\"),\n    }\n}\n\npub(crate) fn fetch_markets(market_type: MarketType) -> Result<Vec<Market>> {\n    match market_type {\n        MarketType::Spot => fetch_spot_markets(),\n        MarketType::InverseFuture => fetch_inverse_future_markets(),\n        MarketType::LinearFuture => fetch_linear_future_markets(),\n        MarketType::InverseSwap => fetch_inverse_swap_markets(),\n        MarketType::LinearSwap => fetch_linear_swap_markets(),\n        MarketType::EuropeanOption => fetch_option_markets(),\n        _ => panic!(\"Unsupported market_type: {market_type}\"),\n    }\n}\n\n// see <https://www.okx.com/docs-v5/en/#rest-api-public-data-get-instruments>\n#[derive(Serialize, Deserialize)]\n#[allow(non_snake_case)]\nstruct RawMarket {\n    instType: String, // Instrument type\n    instId: String,   // Instrument ID, e.g. BTC-USD-SWAP\n    uly: String,      // Underlying, e.g. BTC-USD. Only applicable to FUTURES/SWAP/OPTION\n    category: String, // Fee schedule\n    baseCcy: String,  // Base currency, e.g. BTC inBTC-USDT. Only applicable to SPOT\n    quoteCcy: String, // Quote currency, e.g. USDT in BTC-USDT. Only applicable to SPOT\n    settleCcy: String, /* Settlement and margin currency, e.g. BTC. Only applicable to\n                       * FUTURES/SWAP/OPTION */\n    ctVal: String,    // Contract value. Only applicable to FUTURES/SWAP/OPTION\n    ctMult: String,   // Contract multiplier. Only applicable to FUTURES/SWAP/OPTION\n    ctValCcy: String, // Contract value currency. Only applicable to FUTURES/SWAP/OPTION\n    optType: String,  // Option type, C: Call P: put. Only applicable to OPTION\n    stk: String,      // Strike price. Only applicable to OPTION\n    listTime: String, // Listing time, Unix timestamp format in milliseconds, e.g. 1597026383085\n    expTime: String,  /* Expiry time, Unix timestamp format in milliseconds, e.g. 1597026383085.\n                       * Only applicable to FUTURES/OPTION */\n    lever: String,  // Max Leverage. Not applicable to SPOT、OPTION\n    tickSz: String, // Tick size, e.g. 0.0001\n    lotSz: String,  // Lot size, e.g. BTC-USDT-SWAP: 1\n    minSz: String,  // Minimum order size\n    ctType: String, // Contract type, linear, inverse. Only applicable to FUTURES/SWAP\n    alias: String,  /* Alias, this_week, next_week, quarter, next_quarter. Only applicable to\n                     * FUTURES */\n    state: String, // Instrument status, live, suspend, preopen, settlement\n    #[serde(flatten)]\n    extra: HashMap<String, Value>,\n}\n\nimpl RawMarket {\n    fn to_market(&self) -> Market {\n        let pair = crypto_pair::normalize_pair(self.instId.as_str(), \"okx\").unwrap();\n        let (base, quote) = {\n            let v: Vec<&str> = pair.split('/').collect();\n            (v[0].to_string(), v[1].to_string())\n        };\n        let (market_type, base_id, quote_id) = if self.instType == \"SPOT\" {\n            (MarketType::Spot, self.baseCcy.clone(), self.quoteCcy.clone())\n        } else if self.instType == \"FUTURES\" {\n            if self.ctType == \"linear\" {\n                (MarketType::LinearFuture, self.ctValCcy.clone(), self.settleCcy.clone())\n            } else if self.ctType == \"inverse\" {\n                (MarketType::InverseFuture, self.settleCcy.clone(), self.ctValCcy.clone())\n            } else {\n                panic!(\"Unsupported ctType: {}\", self.ctType);\n            }\n        } else if self.instType == \"SWAP\" {\n            if self.ctType == \"linear\" {\n                (MarketType::LinearSwap, self.ctValCcy.clone(), self.settleCcy.clone())\n            } else if self.ctType == \"inverse\" {\n                (MarketType::InverseSwap, self.settleCcy.clone(), self.ctValCcy.clone())\n            } else {\n                panic!(\"Unsupported ctType: {}\", self.ctType);\n            }\n        } else if self.instType == \"OPTION\" {\n            (MarketType::EuropeanOption, self.settleCcy.clone(), \"USD\".to_string())\n        } else {\n            panic!(\"Unsupported market_type: {}\", self.instType);\n        };\n\n        Market {\n            exchange: \"okx\".to_string(),\n            market_type,\n            symbol: self.instId.to_string(),\n            base_id,\n            quote_id,\n            settle_id: if self.instType == \"SPOT\" { None } else { Some(self.settleCcy.clone()) },\n            base,\n            quote,\n            settle: if self.instType == \"SPOT\" { None } else { Some(self.settleCcy.clone()) },\n            active: self.state == \"live\",\n            margin: !self.lever.is_empty(),\n            // see https://www.okx.com/fees.html\n            fees: Fees {\n                maker: if self.instType == \"SPOT\" { 0.0008 } else { 0.0002 },\n                taker: if self.instType == \"SPOT\" { 0.001 } else { 0.0005 },\n            },\n            precision: Precision {\n                tick_size: self.tickSz.parse::<f64>().unwrap(),\n                lot_size: self.lotSz.parse::<f64>().unwrap(),\n            },\n            quantity_limit: Some(QuantityLimit {\n                min: self.minSz.parse::<f64>().ok(),\n                max: None,\n                notional_min: None,\n                notional_max: None,\n            }),\n            contract_value: if self.instType == \"SPOT\" {\n                None\n            } else {\n                Some(self.ctVal.parse::<f64>().unwrap())\n            },\n            delivery_date: if self.instType == \"FUTURES\" || self.instType == \"OPTION\" {\n                Some(self.expTime.parse::<u64>().unwrap())\n            } else {\n                None\n            },\n            info: serde_json::to_value(self).unwrap().as_object().unwrap().clone(),\n        }\n    }\n}\n\n// Retrieve a list of instruments.\n//\n// see <https://www.okx.com/docs-v5/en/#rest-api-public-data-get-instruments>\n// instType: SPOT, MARGIN, SWAP, FUTURES, OPTION\nfn fetch_raw_markets_raw(inst_type: &str) -> Result<Vec<RawMarket>> {\n    let markets = if inst_type == \"OPTION\" {\n        let underlying_indexes = {\n            let txt =\n                http_get(\"https://www.okx.com/api/v5/public/underlying?instType=OPTION\", None)?;\n            let json_obj = serde_json::from_str::<HashMap<String, Value>>(&txt).unwrap();\n            let data = json_obj.get(\"data\").unwrap().as_array().unwrap()[0].as_array().unwrap();\n            data.iter().map(|x| x.as_str().unwrap().to_string()).collect::<Vec<String>>()\n        };\n\n        let mut markets = Vec::<RawMarket>::new();\n        for underlying in underlying_indexes.iter() {\n            let url = format!(\n                \"https://www.okx.com/api/v5/public/instruments?instType=OPTION&uly={underlying}\"\n            );\n            let txt = {\n                let txt = http_get(url.as_str(), None)?;\n                let json_obj = serde_json::from_str::<HashMap<String, Value>>(&txt).unwrap();\n                serde_json::to_string(json_obj.get(\"data\").unwrap()).unwrap()\n            };\n            let mut arr = serde_json::from_str::<Vec<RawMarket>>(&txt).unwrap();\n            markets.append(&mut arr);\n        }\n\n        markets\n    } else {\n        let url = format!(\"https://www.okx.com/api/v5/public/instruments?instType={inst_type}\");\n        let txt = {\n            let txt = http_get(url.as_str(), None)?;\n            let json_obj = serde_json::from_str::<HashMap<String, Value>>(&txt).unwrap();\n            serde_json::to_string(json_obj.get(\"data\").unwrap()).unwrap()\n        };\n        serde_json::from_str::<Vec<RawMarket>>(&txt).unwrap()\n    };\n    Ok(markets.into_iter().filter(|x| x.state == \"live\").collect())\n}\n\nfn fetch_spot_symbols() -> Result<Vec<String>> {\n    let symbols =\n        fetch_raw_markets_raw(\"SPOT\")?.into_iter().map(|m| m.instId).collect::<Vec<String>>();\n    Ok(symbols)\n}\n\nfn fetch_inverse_future_symbols() -> Result<Vec<String>> {\n    let symbols = fetch_raw_markets_raw(\"FUTURES\")?\n        .into_iter()\n        .filter(|m| m.ctType == \"inverse\")\n        .map(|m| m.instId)\n        .collect::<Vec<String>>();\n    Ok(symbols)\n}\n\nfn fetch_linear_future_symbols() -> Result<Vec<String>> {\n    let symbols = fetch_raw_markets_raw(\"FUTURES\")?\n        .into_iter()\n        .filter(|m| m.ctType == \"linear\")\n        .map(|m| m.instId)\n        .collect::<Vec<String>>();\n    Ok(symbols)\n}\n\nfn fetch_inverse_swap_symbols() -> Result<Vec<String>> {\n    let symbols = fetch_raw_markets_raw(\"SWAP\")?\n        .into_iter()\n        .filter(|m| m.ctType == \"inverse\")\n        .map(|m| m.instId)\n        .collect::<Vec<String>>();\n    Ok(symbols)\n}\n\nfn fetch_linear_swap_symbols() -> Result<Vec<String>> {\n    let symbols = fetch_raw_markets_raw(\"SWAP\")?\n        .into_iter()\n        .filter(|m| m.ctType == \"linear\")\n        .map(|m| m.instId)\n        .collect::<Vec<String>>();\n    Ok(symbols)\n}\n\nfn fetch_option_symbols() -> Result<Vec<String>> {\n    let symbols =\n        fetch_raw_markets_raw(\"OPTION\")?.into_iter().map(|m| m.instId).collect::<Vec<String>>();\n    Ok(symbols)\n}\n\nfn fetch_spot_markets() -> Result<Vec<Market>> {\n    let markets =\n        fetch_raw_markets_raw(\"SPOT\")?.into_iter().map(|m| m.to_market()).collect::<Vec<Market>>();\n    Ok(markets)\n}\n\nfn fetch_inverse_future_markets() -> Result<Vec<Market>> {\n    let markets = fetch_raw_markets_raw(\"FUTURES\")?\n        .into_iter()\n        .filter(|m| m.ctType == \"inverse\")\n        .map(|m| m.to_market())\n        .collect::<Vec<Market>>();\n    Ok(markets)\n}\n\nfn fetch_linear_future_markets() -> Result<Vec<Market>> {\n    let markets = fetch_raw_markets_raw(\"FUTURES\")?\n        .into_iter()\n        .filter(|m| m.ctType == \"linear\")\n        .map(|m| m.to_market())\n        .collect::<Vec<Market>>();\n    Ok(markets)\n}\n\nfn fetch_inverse_swap_markets() -> Result<Vec<Market>> {\n    let markets = fetch_raw_markets_raw(\"SWAP\")?\n        .into_iter()\n        .filter(|m| m.ctType == \"inverse\")\n        .map(|m| m.to_market())\n        .collect::<Vec<Market>>();\n    Ok(markets)\n}\n\nfn fetch_linear_swap_markets() -> Result<Vec<Market>> {\n    let markets = fetch_raw_markets_raw(\"SWAP\")?\n        .into_iter()\n        .filter(|m| m.ctType == \"linear\")\n        .map(|m| m.to_market())\n        .collect::<Vec<Market>>();\n    Ok(markets)\n}\n\nfn fetch_option_markets() -> Result<Vec<Market>> {\n    let markets = fetch_raw_markets_raw(\"OPTION\")?\n        .into_iter()\n        .map(|m| m.to_market())\n        .collect::<Vec<Market>>();\n    Ok(markets)\n}\n"
  },
  {
    "path": "crypto-markets/src/exchanges/utils.rs",
    "content": "use reqwest::header;\n\nuse crate::error::{Error, Result};\nuse std::collections::HashMap;\n\npub(super) fn http_get(url: &str, params: Option<&HashMap<String, String>>) -> Result<String> {\n    let mut full_url = url.to_string();\n    if let Some(params) = params {\n        let mut first = true;\n        for (k, v) in params.iter() {\n            if first {\n                full_url.push_str(format!(\"?{k}={v}\").as_str());\n                first = false;\n            } else {\n                full_url.push_str(format!(\"&{k}={v}\").as_str());\n            }\n        }\n    }\n    // println!(\"{}\", full_url);\n\n    let mut headers = header::HeaderMap::new();\n    headers.insert(header::CONTENT_TYPE, header::HeaderValue::from_static(\"application/json\"));\n\n    let client = reqwest::blocking::Client::builder()\n         .default_headers(headers)\n         .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\")\n         .gzip(true)\n         .build()?;\n    let response = client.get(full_url.as_str()).send()?;\n\n    match response.error_for_status() {\n        Ok(resp) => Ok(resp.text()?),\n        Err(error) => Err(Error::from(error)),\n    }\n}\n\n#[allow(dead_code)]\nfn precision_from_string(s: &str) -> i64 {\n    if let Some(dot_pos) = s.find('.') {\n        let mut none_zero = 0;\n        for (i, ch) in s.chars().rev().enumerate() {\n            if ch != '0' {\n                none_zero = s.len() - 1 - i;\n                break;\n            }\n        }\n        (none_zero - dot_pos) as i64\n    } else {\n        0\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use std::collections::HashMap;\n\n    use serde_json::Value;\n\n    // System proxies are enabled by default, see <https://docs.rs/reqwest/latest/reqwest/#proxies>\n    #[test]\n    #[ignore]\n    fn use_system_socks_proxy() {\n        std::env::set_var(\"https_proxy\", \"socks5://127.0.0.1:9050\");\n        let text = super::http_get(\"https://check.torproject.org/api/ip\", None).unwrap();\n        let obj = serde_json::from_str::<HashMap<String, Value>>(&text).unwrap();\n        assert!(obj.get(\"IsTor\").unwrap().as_bool().unwrap());\n    }\n\n    #[test]\n    #[ignore]\n    fn use_system_https_proxy() {\n        std::env::set_var(\"https_proxy\", \"http://127.0.0.1:8118\");\n        let text = super::http_get(\"https://check.torproject.org/api/ip\", None).unwrap();\n        let obj = serde_json::from_str::<HashMap<String, Value>>(&text).unwrap();\n        assert!(obj.get(\"IsTor\").unwrap().as_bool().unwrap());\n    }\n\n    #[test]\n    fn test_calc_precision() {\n        assert_eq!(4, super::precision_from_string(\"0.000100\"));\n        assert_eq!(0, super::precision_from_string(\"10.00000000\"));\n    }\n}\n"
  },
  {
    "path": "crypto-markets/src/exchanges/zb/mod.rs",
    "content": "mod zb_spot;\nmod zb_swap;\n\nuse crate::{error::Result, Market, MarketType};\n\npub(crate) fn fetch_symbols(market_type: MarketType) -> Result<Vec<String>> {\n    match market_type {\n        MarketType::Spot => zb_spot::fetch_spot_symbols(),\n        MarketType::LinearSwap => zb_swap::fetch_linear_swap_symbols(),\n        _ => panic!(\"Unkown market_type: {market_type}\"),\n    }\n}\n\npub(crate) fn fetch_markets(market_type: MarketType) -> Result<Vec<Market>> {\n    match market_type {\n        MarketType::Spot => zb_spot::fetch_spot_markets(),\n        MarketType::LinearSwap => zb_swap::fetch_linear_swap_markets(),\n        _ => panic!(\"Unkown market_type: {market_type}\"),\n    }\n}\n"
  },
  {
    "path": "crypto-markets/src/exchanges/zb/zb_spot.rs",
    "content": "use std::collections::HashMap;\n\nuse super::super::utils::http_get;\nuse crate::{error::Result, Fees, Market, Precision, QuantityLimit};\n\nuse crypto_market_type::MarketType;\nuse serde::{Deserialize, Serialize};\nuse serde_json::Value;\n\n#[derive(Serialize, Deserialize)]\n#[allow(non_snake_case)]\nstruct SpotMarket {\n    #[serde(default)]\n    symbol: String,\n    amountScale: u32,\n    minAmount: f64,\n    minSize: f64,\n    priceScale: u32,\n    #[serde(flatten)]\n    extra: HashMap<String, Value>,\n}\n\n// See https://zbgapi.github.io/docs/spot/v1/en/#public-get-all-supported-trading-symbols\nfn fetch_spot_markets_raw() -> Result<Vec<SpotMarket>> {\n    let txt = http_get(\"https://api.zb.com/data/v1/markets\", None)?;\n    let m = serde_json::from_str::<HashMap<String, SpotMarket>>(&txt)?;\n    let mut markets = Vec::new();\n    for (symbol, mut market) in m {\n        market.symbol = symbol;\n        markets.push(market);\n    }\n    Ok(markets)\n}\n\npub(super) fn fetch_spot_symbols() -> Result<Vec<String>> {\n    let markets = fetch_spot_markets_raw()?;\n    let symbols: Vec<String> = markets.into_iter().map(|m| m.symbol).collect();\n    Ok(symbols)\n}\n\npub(super) fn fetch_spot_markets() -> Result<Vec<Market>> {\n    let markets: Vec<Market> = fetch_spot_markets_raw()?\n        .into_iter()\n        .map(|m| {\n            let info = serde_json::to_value(&m).unwrap().as_object().unwrap().clone();\n            let (base_id, quote_id) = {\n                let v: Vec<&str> = m.symbol.split('_').collect();\n                (v[0].to_string(), v[1].to_string())\n            };\n            let pair = crypto_pair::normalize_pair(&m.symbol, \"zb\").unwrap();\n            let (base, quote) = {\n                let v: Vec<&str> = pair.split('/').collect();\n                (v[0].to_string(), v[1].to_string())\n            };\n            Market {\n                exchange: \"zb\".to_string(),\n                market_type: MarketType::Spot,\n                symbol: m.symbol,\n                base_id,\n                quote_id,\n                settle_id: None,\n                base,\n                quote,\n                settle: None,\n                active: true,\n                margin: false,\n                // see https://www.zb.com/help/rate/6\n                fees: Fees { maker: 0.002, taker: 0.002 },\n                precision: Precision {\n                    tick_size: 1.0 / (10_i64.pow(m.priceScale) as f64),\n                    lot_size: 1.0 / (10_i64.pow(m.amountScale) as f64),\n                },\n                quantity_limit: Some(QuantityLimit {\n                    min: Some(m.minAmount),\n                    max: None,\n                    notional_min: None,\n                    notional_max: None,\n                }),\n                contract_value: None,\n                delivery_date: None,\n                info,\n            }\n        })\n        .collect();\n    Ok(markets)\n}\n"
  },
  {
    "path": "crypto-markets/src/exchanges/zb/zb_swap.rs",
    "content": "use std::collections::HashMap;\n\nuse super::super::utils::http_get;\nuse crate::{\n    error::{Error, Result},\n    Fees, Market, Precision, QuantityLimit,\n};\n\nuse crypto_market_type::MarketType;\nuse serde::{Deserialize, Serialize};\nuse serde_json::Value;\n\n#[derive(Serialize, Deserialize)]\n#[allow(non_snake_case)]\nstruct SwapMarket {\n    symbol: String,\n    marginCurrencyName: String,\n    buyerCurrencyName: String,\n    sellerCurrencyName: String,\n    canTrade: bool,\n    canOpenPosition: bool,\n    amountDecimal: u32,\n    priceDecimal: u32,\n    minAmount: String,\n    maxAmount: String,\n    status: i64,\n    #[serde(flatten)]\n    extra: HashMap<String, Value>,\n}\n\n#[derive(Serialize, Deserialize)]\n#[allow(non_snake_case)]\nstruct Response {\n    code: i64,\n    data: Vec<SwapMarket>,\n}\n\n// See https://github.com/ZBFuture/docs/blob/main/API%20V2%20_en.md#71-trading-pair\nfn fetch_swap_markets_internal(url: &str) -> Result<Vec<SwapMarket>> {\n    let txt = http_get(url, None)?;\n    let resp = serde_json::from_str::<Response>(&txt).unwrap();\n    if resp.code == 10000 {\n        Ok(resp\n            .data\n            .into_iter()\n            .filter(|m| m.canTrade && m.canOpenPosition && m.status == 1)\n            .collect())\n    } else {\n        Err(Error(txt))\n    }\n}\n\nfn fetch_swap_markets_raw() -> Result<Vec<SwapMarket>> {\n    let usdt_markets =\n        fetch_swap_markets_internal(\"https://fapi.zb.com/Server/api/v2/config/marketList\")?;\n    Ok(usdt_markets)\n    // let qc_markets =\n    //     fetch_swap_markets_internal(\"https://fapi.zb.com/qc/Server/api/v2/config/marketList\")?;\n    // Ok(usdt_markets\n    //     .into_iter()\n    //     .chain(qc_markets.into_iter())\n    //     .collect())\n}\n\npub(super) fn fetch_linear_swap_symbols() -> Result<Vec<String>> {\n    let symbols = fetch_swap_markets_raw()?.into_iter().map(|m| m.symbol).collect::<Vec<String>>();\n    Ok(symbols)\n}\n\nfn to_market(raw_market: &SwapMarket) -> Market {\n    let (base_id, quote_id) = {\n        let v: Vec<&str> = raw_market.symbol.split('_').collect();\n        (v[0].to_string(), v[1].to_string())\n    };\n\n    let pair = crypto_pair::normalize_pair(&raw_market.symbol, \"zb\").unwrap();\n    let (base, quote) = {\n        let v: Vec<&str> = pair.split('/').collect();\n        (v[0].to_string(), v[1].to_string())\n    };\n\n    Market {\n        exchange: \"zb\".to_string(),\n        market_type: MarketType::LinearSwap,\n        symbol: raw_market.symbol.to_string(),\n        base_id,\n        quote_id,\n        settle_id: Some(raw_market.marginCurrencyName.to_uppercase()),\n        base,\n        quote,\n        settle: Some(raw_market.marginCurrencyName.to_uppercase()),\n        active: true,\n        margin: true,\n        // see https://www.zb.com/help/rate/20\n        fees: Fees { maker: 0.0005, taker: 0.00075 },\n        precision: Precision {\n            tick_size: 1.0 / (10_i64.pow(raw_market.priceDecimal) as f64),\n            lot_size: 1.0 / (10_i64.pow(raw_market.amountDecimal) as f64),\n        },\n        quantity_limit: Some(QuantityLimit {\n            min: raw_market.minAmount.parse::<f64>().ok(),\n            max: Some(raw_market.maxAmount.parse::<f64>().unwrap()),\n            notional_min: None,\n            notional_max: None,\n        }),\n        contract_value: Some(1.0),\n        delivery_date: None,\n        info: serde_json::to_value(raw_market).unwrap().as_object().unwrap().clone(),\n    }\n}\n\npub(super) fn fetch_linear_swap_markets() -> Result<Vec<Market>> {\n    let markets =\n        fetch_swap_markets_raw()?.into_iter().map(|m| to_market(&m)).collect::<Vec<Market>>();\n    Ok(markets)\n}\n"
  },
  {
    "path": "crypto-markets/src/exchanges/zbg/mod.rs",
    "content": "mod zbg_spot;\nmod zbg_swap;\n\nuse crate::{error::Result, Market, MarketType};\n\npub(crate) fn fetch_symbols(market_type: MarketType) -> Result<Vec<String>> {\n    match market_type {\n        MarketType::Spot => zbg_spot::fetch_spot_symbols(),\n        MarketType::InverseSwap => zbg_swap::fetch_inverse_swap_symbols(),\n        MarketType::LinearSwap => zbg_swap::fetch_linear_swap_symbols(),\n        _ => panic!(\"Unkown market_type: {market_type}\"),\n    }\n}\n\npub(crate) fn fetch_markets(market_type: MarketType) -> Result<Vec<Market>> {\n    match market_type {\n        MarketType::Spot => zbg_spot::fetch_spot_markets(),\n        MarketType::InverseSwap => zbg_swap::fetch_inverse_swap_markets(),\n        MarketType::LinearSwap => zbg_swap::fetch_linear_swap_markets(),\n        _ => panic!(\"Unkown market_type: {market_type}\"),\n    }\n}\n"
  },
  {
    "path": "crypto-markets/src/exchanges/zbg/zbg_spot.rs",
    "content": "use std::collections::HashMap;\n\nuse super::super::utils::http_get;\nuse crate::{\n    error::{Error, Result},\n    Fees, Market, Precision, QuantityLimit,\n};\n\nuse crypto_market_type::MarketType;\nuse serde::{Deserialize, Serialize};\nuse serde_json::Value;\n\n#[derive(Serialize, Deserialize)]\n#[serde(rename_all = \"kebab-case\")]\nstruct SpotMarket {\n    symbol: String,\n    symbol_partition: String,\n    price_precision: i64,\n    min_order_amt: String,\n    id: String,\n    state: String,\n    base_currency: String,\n    amount_precision: i64,\n    max_order_amt: Option<String>,\n    quote_currency: String,\n    #[serde(flatten)]\n    extra: HashMap<String, Value>,\n}\n\n#[derive(Serialize, Deserialize)]\nstruct ResMsg {\n    message: String,\n    method: Option<String>,\n    code: String,\n}\n\n#[derive(Serialize, Deserialize)]\n#[allow(non_snake_case)]\nstruct Response {\n    datas: Vec<SpotMarket>,\n    resMsg: ResMsg,\n}\n\n// See https://zbgapi.github.io/docs/spot/v1/en/#public-get-all-supported-trading-symbols\nfn fetch_spot_markets_raw() -> Result<Vec<SpotMarket>> {\n    let txt = http_get(\"https://www.zbg.com/exchange/api/v1/common/symbols\", None)?;\n    let resp = serde_json::from_str::<Response>(&txt)?;\n    if resp.resMsg.code != \"1\" {\n        Err(Error(txt))\n    } else {\n        let valid: Vec<SpotMarket> =\n            resp.datas.into_iter().filter(|x| x.state == \"online\").collect();\n        Ok(valid)\n    }\n}\n\npub(super) fn fetch_spot_symbols() -> Result<Vec<String>> {\n    let markets = fetch_spot_markets_raw()?;\n    let symbols: Vec<String> = markets.into_iter().map(|m| m.symbol).collect();\n    Ok(symbols)\n}\n\npub(super) fn fetch_spot_markets() -> Result<Vec<Market>> {\n    let markets: Vec<Market> = fetch_spot_markets_raw()?\n        .into_iter()\n        .map(|m| {\n            let info = serde_json::to_value(&m).unwrap().as_object().unwrap().clone();\n            let pair = crypto_pair::normalize_pair(&m.symbol, \"zbg\").unwrap();\n            let (base, quote) = {\n                let v: Vec<&str> = pair.split('/').collect();\n                (v[0].to_string(), v[1].to_string())\n            };\n            Market {\n                exchange: \"zbg\".to_string(),\n                market_type: MarketType::Spot,\n                symbol: m.symbol,\n                base_id: m.base_currency,\n                quote_id: m.quote_currency,\n                settle_id: None,\n                base,\n                quote,\n                settle: None,\n                active: m.state == \"online\",\n                margin: false,\n                // TODO: need to find zbg spot fees\n                fees: Fees { maker: 0.002, taker: 0.002 },\n                precision: Precision {\n                    tick_size: 1.0 / (10_i64.pow(m.price_precision as u32) as f64),\n                    lot_size: 1.0 / (10_i64.pow(m.amount_precision as u32) as f64),\n                },\n                quantity_limit: Some(QuantityLimit {\n                    min: m.min_order_amt.parse::<f64>().ok(),\n                    max: None,\n                    notional_min: None,\n                    notional_max: None,\n                }),\n                contract_value: None,\n                delivery_date: None,\n                info,\n            }\n        })\n        .collect();\n    Ok(markets)\n}\n"
  },
  {
    "path": "crypto-markets/src/exchanges/zbg/zbg_swap.rs",
    "content": "use std::collections::HashMap;\n\nuse super::super::utils::http_get;\nuse crate::{\n    error::{Error, Result},\n    Fees, Market, Precision,\n};\n\nuse crypto_market_type::MarketType;\nuse serde::{Deserialize, Serialize};\nuse serde_json::Value;\n\n#[derive(Serialize, Deserialize)]\n#[allow(non_snake_case)]\nstruct SwapMarket {\n    symbol: String,\n    currencyName: String,\n    lotSize: String,\n    contractId: i64,\n    takerFeeRatio: String,\n    commodityId: i64,\n    currencyId: i64,\n    contractUnit: String,\n    makerFeeRatio: String,\n    priceTick: String,\n    commodityName: Option<String>,\n    #[serde(flatten)]\n    extra: HashMap<String, Value>,\n}\n\n#[derive(Serialize, Deserialize)]\nstruct ResMsg {\n    message: String,\n    method: Option<String>,\n    code: String,\n}\n\n#[derive(Serialize, Deserialize)]\n#[allow(non_snake_case)]\nstruct Response {\n    datas: Vec<SwapMarket>,\n    resMsg: ResMsg,\n}\n\n// See https://zbgapi.github.io/docs/future/v1/en/#public-get-contracts\nfn fetch_swap_markets_raw() -> Result<Vec<SwapMarket>> {\n    let txt = http_get(\"https://www.zbg.com/exchange/api/v1/future/common/contracts\", None)?;\n    let resp = serde_json::from_str::<Response>(&txt)?;\n    if resp.resMsg.code != \"1\" { Err(Error(txt)) } else { Ok(resp.datas) }\n}\n\npub(super) fn fetch_inverse_swap_symbols() -> Result<Vec<String>> {\n    let symbols = fetch_swap_markets_raw()?\n        .into_iter()\n        .map(|m| m.symbol)\n        .filter(|x| x.ends_with(\"_USD-R\"))\n        .collect::<Vec<String>>();\n    Ok(symbols)\n}\n\npub(super) fn fetch_linear_swap_symbols() -> Result<Vec<String>> {\n    let symbols = fetch_swap_markets_raw()?\n        .into_iter()\n        .map(|m| m.symbol)\n        .filter(|x| x.ends_with(\"_USDT\") || x.ends_with(\"_ZUSD\"))\n        .collect::<Vec<String>>();\n    Ok(symbols)\n}\n\nfn to_market(raw_market: &SwapMarket) -> Market {\n    let pair = crypto_pair::normalize_pair(&raw_market.symbol, \"zbg\").unwrap();\n    let (base, quote) = {\n        let v: Vec<&str> = pair.split('/').collect();\n        (v[0].to_string(), v[1].to_string())\n    };\n    let (base_id, quote_id) = {\n        let v: Vec<&str> = raw_market.symbol.split('_').collect();\n        (\n            v[0].to_string(),\n            if v[1].ends_with(\"-R\") {\n                v[1].strip_suffix(\"-R\").unwrap().to_string()\n            } else {\n                v[1].to_string()\n            },\n        )\n    };\n    let market_type = if raw_market.symbol.ends_with(\"_USD-R\") {\n        MarketType::InverseSwap\n    } else if raw_market.symbol.ends_with(\"_USDT\") {\n        MarketType::LinearSwap\n    } else {\n        panic!(\n            \"Failed to detect market_type {}\",\n            serde_json::to_string_pretty(raw_market).unwrap()\n        );\n    };\n\n    Market {\n        exchange: \"zbg\".to_string(),\n        market_type,\n        symbol: raw_market.symbol.to_string(),\n        base_id,\n        quote_id,\n        settle_id: Some(raw_market.currencyName.to_uppercase()),\n        base,\n        quote,\n        settle: Some(raw_market.currencyName.to_uppercase()),\n        active: true,\n        margin: true,\n        fees: Fees {\n            maker: raw_market.makerFeeRatio.parse::<f64>().unwrap(),\n            taker: raw_market.takerFeeRatio.parse::<f64>().unwrap(),\n        },\n        precision: Precision {\n            tick_size: raw_market.priceTick.parse::<f64>().unwrap(),\n            lot_size: raw_market.lotSize.parse::<f64>().unwrap(),\n        },\n        quantity_limit: None,\n        contract_value: Some(raw_market.contractUnit.parse::<f64>().unwrap()),\n        delivery_date: None,\n        info: serde_json::to_value(raw_market).unwrap().as_object().unwrap().clone(),\n    }\n}\n\npub(super) fn fetch_inverse_swap_markets() -> Result<Vec<Market>> {\n    let markets = fetch_swap_markets_raw()?\n        .into_iter()\n        .filter(|m| m.symbol.ends_with(\"_USD-R\"))\n        .map(|m| to_market(&m))\n        .collect::<Vec<Market>>();\n    Ok(markets)\n}\n\npub(super) fn fetch_linear_swap_markets() -> Result<Vec<Market>> {\n    let markets = fetch_swap_markets_raw()?\n        .into_iter()\n        .filter(|m| m.symbol.ends_with(\"_USDT\"))\n        .map(|m| to_market(&m))\n        .collect::<Vec<Market>>();\n    Ok(markets)\n}\n"
  },
  {
    "path": "crypto-markets/src/lib.rs",
    "content": "#![allow(clippy::unnecessary_wraps)]\n//! Get all trading pairs of a cryptocurrency exchange.\n//!\n//! ## Example\n//!\n//! ```\n//! use crypto_markets::fetch_markets;\n//! use crypto_market_type::MarketType;\n//!\n//! let markets = fetch_markets(\"binance\", MarketType::Spot).unwrap();\n//! println!(\"{}\", serde_json::to_string_pretty(&markets).unwrap())\n//! ```\n\nmod error;\nmod exchanges;\nmod market;\n\nuse crypto_market_type::MarketType;\npub use error::Error;\npub use market::{Fees, Market, Precision, QuantityLimit};\n\nuse error::Result;\n\n/// Fetch trading symbols.\npub fn fetch_symbols(exchange: &str, market_type: MarketType) -> Result<Vec<String>> {\n    match exchange {\n        \"binance\" => exchanges::binance::fetch_symbols(market_type),\n        \"bitfinex\" => exchanges::bitfinex::fetch_symbols(market_type),\n        \"bitget\" => exchanges::bitget::fetch_symbols(market_type),\n        \"bithumb\" => exchanges::bithumb::fetch_symbols(market_type),\n        \"bitmex\" => exchanges::bitmex::fetch_symbols(market_type),\n        \"bitstamp\" => exchanges::bitstamp::fetch_symbols(market_type),\n        \"bitz\" => exchanges::bitz::fetch_symbols(market_type),\n        \"bybit\" => exchanges::bybit::fetch_symbols(market_type),\n        \"coinbase_pro\" => exchanges::coinbase_pro::fetch_symbols(market_type),\n        \"deribit\" => exchanges::deribit::fetch_symbols(market_type),\n        \"dydx\" => exchanges::dydx::fetch_symbols(market_type),\n        \"ftx\" => exchanges::ftx::fetch_symbols(market_type),\n        \"gate\" => exchanges::gate::fetch_symbols(market_type),\n        \"huobi\" => exchanges::huobi::fetch_symbols(market_type),\n        \"kraken\" => exchanges::kraken::fetch_symbols(market_type),\n        \"kucoin\" => exchanges::kucoin::fetch_symbols(market_type),\n        \"mexc\" => exchanges::mexc::fetch_symbols(market_type),\n        \"okx\" => exchanges::okx::fetch_symbols(market_type),\n        \"zb\" => exchanges::zb::fetch_symbols(market_type),\n        \"zbg\" => exchanges::zbg::fetch_symbols(market_type),\n        _ => panic!(\"Unsupported exchange {exchange}\"),\n    }\n}\n\n/// Fetch trading markets of a cryptocurrency exchange.\n///\n/// # Arguments\n///\n/// * `exchange` - The exchange name\n/// * `market_type` - The market type\n///\n/// # Example\n///\n/// ```\n/// use crypto_markets::fetch_markets;\n/// use crypto_market_type::MarketType;\n/// let markets = fetch_markets(\"binance\", MarketType::Spot).unwrap();\n/// assert!(!markets.is_empty());\n/// println!(\"{}\", serde_json::to_string_pretty(&markets).unwrap())\n/// ```\npub fn fetch_markets(exchange: &str, market_type: MarketType) -> Result<Vec<Market>> {\n    match exchange {\n        \"binance\" => exchanges::binance::fetch_markets(market_type),\n        \"bitfinex\" => exchanges::bitfinex::fetch_markets(market_type),\n        \"bitget\" => exchanges::bitget::fetch_markets(market_type),\n        \"bithumb\" => exchanges::bithumb::fetch_markets(market_type),\n        \"bitmex\" => exchanges::bitmex::fetch_markets(market_type),\n        \"bitstamp\" => exchanges::bitstamp::fetch_markets(market_type),\n        \"bitz\" => exchanges::bitz::fetch_markets(market_type),\n        \"bybit\" => exchanges::bybit::fetch_markets(market_type),\n        \"coinbase_pro\" => exchanges::coinbase_pro::fetch_markets(market_type),\n        \"deribit\" => exchanges::deribit::fetch_markets(market_type),\n        \"dydx\" => exchanges::dydx::fetch_markets(market_type),\n        \"ftx\" => exchanges::ftx::fetch_markets(market_type),\n        \"gate\" => exchanges::gate::fetch_markets(market_type),\n        \"huobi\" => exchanges::huobi::fetch_markets(market_type),\n        \"kraken\" => exchanges::kraken::fetch_markets(market_type),\n        \"kucoin\" => exchanges::kucoin::fetch_markets(market_type),\n        \"mexc\" => exchanges::mexc::fetch_markets(market_type),\n        \"okx\" => exchanges::okx::fetch_markets(market_type),\n        \"zb\" => exchanges::zb::fetch_markets(market_type),\n        \"zbg\" => exchanges::zbg::fetch_markets(market_type),\n        _ => panic!(\"Unsupported exchange {exchange}\"),\n    }\n}\n"
  },
  {
    "path": "crypto-markets/src/main.rs",
    "content": "use crypto_market_type::MarketType;\nuse crypto_markets::fetch_markets;\nuse std::{env, str::FromStr};\n\nfn main() {\n    let args: Vec<String> = env::args().collect();\n    if args.len() != 3 {\n        println!(\"Usage: crypto-markets <exchange> <market_type>\");\n        return;\n    }\n\n    let exchange: &str = &args[1];\n    let market_type = MarketType::from_str(&args[2]);\n    if market_type.is_err() {\n        println!(\"Unknown market type: {}\", &args[2]);\n        return;\n    }\n\n    let resp = fetch_markets(exchange, market_type.unwrap());\n    match resp {\n        Ok(markets) => println!(\"{}\", serde_json::to_string_pretty(&markets).unwrap()),\n        Err(err) => println!(\"{err}\"),\n    }\n}\n"
  },
  {
    "path": "crypto-markets/src/market.rs",
    "content": "use crypto_market_type::MarketType;\nuse serde::{Deserialize, Serialize};\nuse serde_json::{Map, Value};\n\n#[derive(Clone, Serialize, Deserialize)]\npub struct Fees {\n    pub maker: f64,\n    pub taker: f64,\n}\n\n#[derive(Clone, Serialize, Deserialize)]\npub struct Precision {\n    /// the minimum price change, see https://en.wikipedia.org/wiki/Tick_size\n    pub tick_size: f64,\n    /// the minimum quantity change\n    pub lot_size: f64,\n}\n\n#[derive(Clone, Serialize, Deserialize, PartialEq)]\npub struct QuantityLimit {\n    /// Minimum base quantity\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub min: Option<f64>,\n    /// Maximum base quantity\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub max: Option<f64>,\n    /// Notional minimum size\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub notional_min: Option<f64>,\n    /// Notional maximum size\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub notional_max: Option<f64>,\n}\n\n/// Market contains all information about a market\n#[derive(Clone, Serialize, Deserialize)]\npub struct Market {\n    /// exchange name\n    pub exchange: String,\n    /// Market type\n    pub market_type: MarketType,\n    /// exchange-specific trading symbol, recognized by RESTful API, equivalent\n    /// to ccxt's Market.id.\n    pub symbol: String,\n    /// exchange-specific base currency\n    pub base_id: String,\n    /// exchange-specific quote currency\n    pub quote_id: String,\n    /// exchange-specific settlement currency, i.e., collateral currency, always\n    /// None for spot markets\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub settle_id: Option<String>,\n    /// unified uppercase string of base fiat or crypto currency\n    pub base: String,\n    /// unified uppercase string of quote fiat or crypto currency\n    pub quote: String,\n    /// settlement currency, i.e., collateral currency, always None for spot\n    /// markets\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub settle: Option<String>,\n    /// market status\n    pub active: bool,\n    /// Margin enabled.\n    ///\n    /// * All contract markets are margin enabled, including future, swap and\n    ///   option.\n    /// * Only a few exchanges have spot market with margin enabled.\n    pub margin: bool,\n    pub fees: Fees,\n    /// number of decimal digits after the dot\n    pub precision: Precision,\n    /// the min and max values of quantity\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub quantity_limit: Option<QuantityLimit>,\n    // The value of one contract, not applicable to sport markets\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub contract_value: Option<f64>,\n    /// Delivery date, unix timestamp in milliseconds, only applicable for\n    /// future and option markets.\n    #[serde(skip_serializing_if = \"Option::is_none\")]\n    pub delivery_date: Option<u64>,\n    /// the original JSON string retrieved from the exchange\n    pub info: Map<String, Value>,\n}\n"
  },
  {
    "path": "crypto-markets/tests/binance.rs",
    "content": "use crypto_market_type::{get_market_types, MarketType};\nuse crypto_markets::{fetch_markets, fetch_symbols};\nuse crypto_pair::get_market_type;\nuse test_case::test_case;\n\n#[macro_use]\nmod utils;\n\nconst EXCHANGE_NAME: &str = \"binance\";\n\n#[test]\nfn fetch_all_symbols() {\n    gen_all_symbols!();\n}\n\n#[test]\nfn fetch_spot_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::Spot).unwrap();\n    assert!(!symbols.is_empty());\n    for symbol in symbols.iter() {\n        assert_eq!(symbol.to_uppercase(), symbol.to_string());\n        assert_eq!(MarketType::Spot, get_market_type(symbol, EXCHANGE_NAME, Some(true)));\n    }\n}\n\n#[test]\nfn fetch_inverse_future_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::InverseFuture).unwrap();\n    assert!(!symbols.is_empty());\n\n    for symbol in symbols.iter() {\n        let date = &symbol[(symbol.len() - 6)..];\n        assert!(date.parse::<i64>().is_ok());\n\n        let quote = &symbol[(symbol.len() - 10)..(symbol.len() - 7)];\n        assert_eq!(quote, \"USD\");\n        assert_eq!(MarketType::InverseFuture, get_market_type(symbol, EXCHANGE_NAME, None));\n    }\n}\n\n#[test]\nfn fetch_linear_future_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::LinearFuture).unwrap();\n    assert!(!symbols.is_empty());\n\n    for symbol in symbols.iter() {\n        let date = &symbol[(symbol.len() - 6)..];\n        assert!(date.parse::<i64>().is_ok());\n\n        let quote = &symbol[(symbol.len() - 11)..(symbol.len() - 7)];\n        assert_eq!(quote, \"USDT\");\n        assert_eq!(MarketType::LinearFuture, get_market_type(symbol, EXCHANGE_NAME, None));\n    }\n}\n\n#[test]\nfn fetch_inverse_swap_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::InverseSwap).unwrap();\n    assert!(!symbols.is_empty());\n\n    for symbol in symbols.iter() {\n        assert!(symbol.ends_with(\"USD_PERP\"));\n        assert_eq!(MarketType::InverseSwap, get_market_type(symbol, EXCHANGE_NAME, None));\n    }\n}\n\n#[test]\nfn fetch_linear_swap_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::LinearSwap).unwrap();\n    assert!(!symbols.is_empty());\n\n    for symbol in symbols.iter() {\n        assert!(symbol.ends_with(\"USDT\") || symbol.ends_with(\"BUSD\"));\n        assert_eq!(MarketType::LinearSwap, get_market_type(symbol, EXCHANGE_NAME, None));\n    }\n}\n\n#[ignore]\n#[test]\nfn fetch_option_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::EuropeanOption).unwrap();\n    assert!(!symbols.is_empty());\n\n    for symbol in symbols.iter() {\n        assert!(symbol.ends_with(\"-P\") || symbol.ends_with(\"-C\"));\n        assert_eq!(MarketType::EuropeanOption, get_market_type(symbol, EXCHANGE_NAME, None));\n    }\n}\n\n#[test]\nfn fetch_spot_markets() {\n    let markets = fetch_markets(EXCHANGE_NAME, MarketType::Spot).unwrap();\n    assert!(!markets.is_empty());\n\n    let btc_usdt = markets.iter().find(|m| m.symbol == \"BTCUSDT\").unwrap().clone();\n    assert!(btc_usdt.contract_value.is_none());\n    assert_eq!(btc_usdt.precision.tick_size, 0.01);\n    assert_eq!(btc_usdt.precision.lot_size, 0.00001);\n    let quantity_limit = btc_usdt.quantity_limit.unwrap();\n    assert_eq!(quantity_limit.min.unwrap(), 0.00001);\n    assert_eq!(quantity_limit.max, Some(9000.0));\n}\n\n#[test]\nfn fetch_inverse_future_markets() {\n    let markets = fetch_markets(EXCHANGE_NAME, MarketType::InverseFuture).unwrap();\n    assert!(!markets.is_empty());\n\n    let btcusd = markets.iter().find(|m| m.symbol.starts_with(\"BTCUSD_\")).unwrap().clone();\n    assert_eq!(btcusd.contract_value, Some(100.0));\n    assert_eq!(btcusd.precision.tick_size, 0.1);\n    assert_eq!(btcusd.precision.lot_size, 1.0);\n    let quantity_limit = btcusd.quantity_limit.unwrap();\n    assert_eq!(quantity_limit.min.unwrap(), 1.0);\n    assert_eq!(quantity_limit.max, Some(1000000.0));\n}\n\n#[test]\nfn fetch_inverse_swap_markets() {\n    let markets = fetch_markets(EXCHANGE_NAME, MarketType::InverseSwap).unwrap();\n    assert!(!markets.is_empty());\n\n    let btcusd_perp = markets.iter().find(|m| m.symbol == \"BTCUSD_PERP\").unwrap().clone();\n    assert_eq!(btcusd_perp.contract_value, Some(100.0));\n    assert_eq!(btcusd_perp.precision.tick_size, 0.1);\n    assert_eq!(btcusd_perp.precision.lot_size, 1.0);\n    let quantity_limit = btcusd_perp.quantity_limit.unwrap();\n    assert_eq!(quantity_limit.min.unwrap(), 1.0);\n    assert_eq!(quantity_limit.max, Some(1000000.0));\n}\n\n#[test]\nfn fetch_linear_swap_markets() {\n    let markets = fetch_markets(EXCHANGE_NAME, MarketType::LinearSwap).unwrap();\n    assert!(!markets.is_empty());\n\n    let btcusdt = markets.iter().find(|m| m.symbol == \"BTCUSDT\").unwrap().clone();\n    assert_eq!(btcusdt.contract_value, Some(1.0));\n    assert_eq!(btcusdt.precision.tick_size, 0.01);\n    assert_eq!(btcusdt.precision.lot_size, 0.001);\n    let quantity_limit = btcusdt.quantity_limit.unwrap();\n    assert_eq!(quantity_limit.min.unwrap(), 0.001);\n    assert_eq!(quantity_limit.max, Some(1000.0));\n}\n\n#[ignore]\n#[test]\nfn fetch_option_markets() {\n    let markets = fetch_markets(EXCHANGE_NAME, MarketType::EuropeanOption).unwrap();\n    assert!(!markets.is_empty());\n\n    let btcusd = markets.iter().find(|m| m.symbol.starts_with(\"BTC-\")).unwrap().clone();\n    assert_eq!(btcusd.contract_value, Some(1.0));\n    assert_eq!(btcusd.precision.tick_size, 0.01);\n    assert_eq!(btcusd.precision.lot_size, 0.0001);\n    let quantity_limit = btcusd.quantity_limit.unwrap();\n    assert_eq!(quantity_limit.min.unwrap(), 0.0002);\n    assert_eq!(quantity_limit.max, Some(10000.0));\n}\n\n#[test_case(MarketType::InverseFuture)]\n#[test_case(MarketType::LinearFuture)]\n#[test_case(MarketType::InverseSwap)]\n#[test_case(MarketType::LinearSwap)]\n// #[test_case(MarketType::EuropeanOption)]\nfn test_contract_values(market_type: MarketType) {\n    check_contract_values!(EXCHANGE_NAME, market_type);\n}\n"
  },
  {
    "path": "crypto-markets/tests/bitfinex.rs",
    "content": "use crypto_market_type::{get_market_types, MarketType};\nuse crypto_markets::{fetch_markets, fetch_symbols};\nuse crypto_pair::get_market_type;\nuse test_case::test_case;\n\n#[macro_use]\nmod utils;\n\nconst EXCHANGE_NAME: &str = \"bitfinex\";\n\n#[test]\nfn fetch_all_symbols() {\n    gen_all_symbols!();\n}\n\n#[test]\nfn fetch_spot_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::Spot).unwrap();\n    assert!(!symbols.is_empty());\n    for symbol in symbols.iter() {\n        assert!(symbol.starts_with('t'));\n        assert_eq!(MarketType::Spot, get_market_type(symbol, EXCHANGE_NAME, None));\n    }\n}\n\n#[test]\nfn fetch_linear_swap_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::LinearSwap).unwrap();\n    assert!(!symbols.is_empty());\n    for symbol in symbols.iter() {\n        assert!(symbol.starts_with('t'));\n        assert!(\n            symbol.ends_with(\"F0:USTF0\")\n                || symbol.ends_with(\"F0:BTCF0\")\n                || symbol.ends_with(\"F0:EUTF0\")\n        );\n        assert_eq!(MarketType::LinearSwap, get_market_type(symbol, EXCHANGE_NAME, None));\n    }\n}\n\n#[test]\nfn fetch_spot_markets() {\n    let markets = fetch_markets(EXCHANGE_NAME, MarketType::Spot).unwrap();\n    assert!(!markets.is_empty());\n\n    let btc_usdt = markets.iter().find(|m| m.symbol == \"tBTCUST\").unwrap().clone();\n    assert_eq!(btc_usdt.precision.tick_size, 0.00001);\n    assert_eq!(btc_usdt.precision.lot_size, 0.00000001);\n    let quantity_limit = btc_usdt.quantity_limit.unwrap();\n    assert_eq!(quantity_limit.min.unwrap(), 0.00006);\n    assert_eq!(quantity_limit.max, Some(2000.0));\n}\n\n#[test]\nfn fetch_linear_swap_markets() {\n    let markets = fetch_markets(EXCHANGE_NAME, MarketType::LinearSwap).unwrap();\n    assert!(!markets.is_empty());\n\n    let btc_usdt = markets.iter().find(|m| m.symbol == \"tBTCF0:USTF0\").unwrap().clone();\n    assert_eq!(btc_usdt.precision.tick_size, 0.00001);\n    assert_eq!(btc_usdt.precision.lot_size, 0.00000001);\n    let quantity_limit = btc_usdt.quantity_limit.unwrap();\n    assert_eq!(quantity_limit.min.unwrap(), 0.00006);\n    assert_eq!(quantity_limit.max, Some(100.0));\n}\n\n#[test_case(MarketType::LinearSwap)]\nfn test_contract_values(market_type: MarketType) {\n    check_contract_values!(EXCHANGE_NAME, market_type);\n}\n"
  },
  {
    "path": "crypto-markets/tests/bitget.rs",
    "content": "use crypto_market_type::{get_market_types, MarketType};\nuse crypto_markets::{fetch_markets, fetch_symbols};\nuse crypto_pair::get_market_type;\n// use test_case::test_case;\n\n#[macro_use]\nmod utils;\n\nconst EXCHANGE_NAME: &str = \"bitget\";\n\n#[test]\nfn fetch_all_symbols() {\n    gen_all_symbols!();\n}\n\n#[test]\nfn fetch_spot_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::Spot).unwrap();\n    assert!(!symbols.is_empty());\n    for symbol in symbols.iter() {\n        assert!(\n            symbol.ends_with(\"USDT_SPBL\")\n                || symbol.ends_with(\"BTC_SPBL\")\n                || symbol.ends_with(\"ETH_SPBL\")\n        );\n        assert_eq!(MarketType::Spot, get_market_type(symbol, EXCHANGE_NAME, None));\n    }\n}\n\n#[test]\nfn fetch_inverse_swap_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::InverseSwap).unwrap();\n    assert!(!symbols.is_empty());\n    for symbol in symbols.iter() {\n        assert!(symbol.ends_with(\"USD_DMCBL\"));\n        assert_eq!(MarketType::InverseSwap, get_market_type(symbol, EXCHANGE_NAME, None));\n    }\n}\n\n#[test]\nfn fetch_linear_swap_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::LinearSwap).unwrap();\n    assert!(!symbols.is_empty());\n    for symbol in symbols.iter() {\n        assert!(symbol.ends_with(\"USDT_UMCBL\") || symbol.ends_with(\"PERP_CMCBL\"));\n        assert_eq!(MarketType::LinearSwap, get_market_type(symbol, EXCHANGE_NAME, None));\n    }\n}\n\n#[test]\nfn fetch_inverse_future_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::InverseFuture).unwrap();\n    assert!(!symbols.is_empty());\n    for symbol in symbols.iter() {\n        println!(\"{symbol}\");\n        assert!(symbol.contains(\"USD_DMCBL_\"));\n        assert_eq!(MarketType::InverseFuture, get_market_type(symbol, EXCHANGE_NAME, None));\n    }\n}\n\n#[test]\nfn fetch_spot_markets() {\n    let markets = fetch_markets(EXCHANGE_NAME, MarketType::Spot).unwrap();\n    assert!(!markets.is_empty());\n\n    let btcusdt = markets.iter().find(|m| m.symbol == \"BTCUSDT_SPBL\").unwrap().clone();\n    assert_eq!(btcusdt.precision.tick_size, 0.01);\n    assert_eq!(btcusdt.precision.lot_size, 0.0001);\n    let quantity_limit = btcusdt.quantity_limit.unwrap();\n    assert_eq!(0.0001, quantity_limit.min.unwrap());\n    assert!(quantity_limit.max.is_none());\n}\n\n#[test]\nfn fetch_inverse_swap_markets() {\n    let markets = fetch_markets(EXCHANGE_NAME, MarketType::InverseSwap).unwrap();\n    assert!(!markets.is_empty());\n\n    let btcusd = markets.iter().find(|m| m.symbol == \"BTCUSD_DMCBL\").unwrap().clone();\n    assert_eq!(btcusd.contract_value, Some(1.0));\n    assert_eq!(btcusd.precision.tick_size, 0.1);\n    assert_eq!(btcusd.precision.lot_size, 0.001);\n    let quantity_limit = btcusd.quantity_limit.unwrap();\n    assert_eq!(0.001, quantity_limit.min.unwrap());\n    assert!(quantity_limit.max.is_none());\n}\n\n#[test]\nfn fetch_linear_swap_markets() {\n    let markets = fetch_markets(EXCHANGE_NAME, MarketType::LinearSwap).unwrap();\n    assert!(!markets.is_empty());\n\n    let btcusdt = markets.iter().find(|m| m.symbol == \"BTCUSDT_UMCBL\").unwrap().clone();\n    assert_eq!(btcusdt.contract_value, Some(1.0));\n    assert_eq!(btcusdt.precision.tick_size, 0.1);\n    assert_eq!(btcusdt.precision.lot_size, 0.001);\n    let quantity_limit = btcusdt.quantity_limit.unwrap();\n    assert_eq!(0.001, quantity_limit.min.unwrap());\n    assert!(quantity_limit.max.is_none());\n}\n\n#[test]\nfn fetch_inverse_future_markets() {\n    let markets = fetch_markets(EXCHANGE_NAME, MarketType::InverseFuture).unwrap();\n    assert!(!markets.is_empty());\n\n    let btcusd = markets.iter().find(|m| m.symbol.contains(\"BTCUSD_DMCBL_\")).unwrap().clone();\n    assert_eq!(btcusd.contract_value, Some(1.0));\n    assert_eq!(btcusd.precision.tick_size, 0.1);\n    assert_eq!(btcusd.precision.lot_size, 0.001);\n    assert!(btcusd.delivery_date.is_some());\n    let quantity_limit = btcusd.quantity_limit.unwrap();\n    assert_eq!(0.001, quantity_limit.min.unwrap());\n    assert!(quantity_limit.max.is_none());\n}\n\n// #[test_case(MarketType::InverseSwap)]\n// #[test_case(MarketType::LinearSwap)]\n// fn test_contract_values(market_type: MarketType) {\n//     check_contract_values!(EXCHANGE_NAME, market_type);\n// }\n"
  },
  {
    "path": "crypto-markets/tests/bithumb.rs",
    "content": "use crypto_market_type::{get_market_types, MarketType};\nuse crypto_markets::{fetch_markets, fetch_symbols};\nuse crypto_pair::get_market_type;\n\n#[macro_use]\nmod utils;\n\nconst EXCHANGE_NAME: &str = \"bithumb\";\n\n#[test]\nfn fetch_all_symbols() {\n    gen_all_symbols!();\n}\n\n#[test]\nfn fetch_spot_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::Spot).unwrap();\n    assert!(!symbols.is_empty());\n\n    for symbol in symbols.iter() {\n        assert!(symbol.contains('-'));\n        assert_eq!(symbol.to_string(), symbol.to_uppercase());\n        assert_eq!(MarketType::Spot, get_market_type(symbol, EXCHANGE_NAME, None));\n    }\n}\n\n#[test]\nfn fetch_spot_markets() {\n    let markets = fetch_markets(EXCHANGE_NAME, MarketType::Spot).unwrap();\n    assert!(!markets.is_empty());\n\n    let btc_usdt = markets.iter().find(|m| m.symbol == \"BTC-USDT\").unwrap().clone();\n    assert_eq!(btc_usdt.precision.tick_size, 0.01);\n    assert_eq!(btc_usdt.precision.lot_size, 0.000001);\n    assert!(btc_usdt.quantity_limit.is_none());\n}\n"
  },
  {
    "path": "crypto-markets/tests/bitmex.rs",
    "content": "use crypto_market_type::MarketType;\nuse crypto_markets::{fetch_markets, fetch_symbols};\nuse crypto_pair::get_market_type;\n\n#[macro_use]\nmod utils;\n\nconst EXCHANGE_NAME: &str = \"bitmex\";\n\n#[test]\nfn fetch_all_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::Unknown).unwrap();\n    assert!(!symbols.is_empty());\n}\n\n#[test]\n#[ignore = \"to avoid 429 Too Many Requests\"]\nfn fetch_spot_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::Spot).unwrap();\n    assert!(!symbols.is_empty());\n    for symbol in symbols.iter() {\n        assert!(symbol.ends_with(\"_USDT\"));\n        assert_eq!(MarketType::Spot, get_market_type(symbol, EXCHANGE_NAME, None));\n    }\n}\n\n#[test]\n#[ignore = \"to avoid 429 Too Many Requests\"]\nfn fetch_inverse_swap_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::InverseSwap).unwrap();\n    assert!(!symbols.is_empty());\n    for symbol in symbols.iter() {\n        assert!(symbol.starts_with(\"XBT\") || symbol.starts_with(\"ETH\"));\n        assert!(symbol.ends_with(\"USD\") || symbol.ends_with(\"EUR\") || symbol.ends_with(\"_ETH\"));\n        assert_eq!(MarketType::InverseSwap, get_market_type(symbol, EXCHANGE_NAME, None));\n    }\n}\n\n#[test]\n#[ignore = \"to avoid 429 Too Many Requests\"]\nfn fetch_linear_swap_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::LinearSwap).unwrap();\n    assert!(!symbols.is_empty());\n    for symbol in symbols.iter() {\n        assert!(symbol.ends_with(\"USDT\"));\n        assert_eq!(MarketType::LinearSwap, get_market_type(symbol, EXCHANGE_NAME, None));\n    }\n}\n\n#[test]\n#[ignore = \"to avoid 429 Too Many Requests\"]\nfn fetch_quanto_swap_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::QuantoSwap).unwrap();\n    assert!(!symbols.is_empty());\n    for symbol in symbols.iter() {\n        assert!(symbol.ends_with(\"USD\") || symbol.ends_with(\"USDT\"));\n        assert_eq!(MarketType::QuantoSwap, get_market_type(symbol, EXCHANGE_NAME, None));\n    }\n}\n\n#[test]\n#[ignore = \"to avoid 429 Too Many Requests\"]\nfn fetch_inverse_future_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::InverseFuture).unwrap();\n    assert!(!symbols.is_empty());\n    for symbol in symbols.iter() {\n        assert!(symbol.starts_with(\"XBT\") || symbol.starts_with(\"ETH\"));\n        let date = if let Some(pos) = symbol.rfind('_') {\n            // e.g., ETHUSDM22_ETH\n            &symbol[(pos - 2)..pos]\n        } else {\n            &symbol[(symbol.len() - 2)..]\n        };\n        assert!(date.parse::<i64>().is_ok());\n        assert_eq!(MarketType::InverseFuture, get_market_type(symbol, EXCHANGE_NAME, None));\n    }\n}\n\n#[test]\n#[ignore = \"to avoid 429 Too Many Requests\"]\nfn fetch_quanto_future_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::QuantoFuture).unwrap();\n    assert!(!symbols.is_empty());\n    for symbol in symbols.iter() {\n        assert!(!symbol.starts_with(\"XBT\"));\n        let date = &symbol[(symbol.len() - 2)..];\n        assert!(date.parse::<i64>().is_ok());\n\n        let quote = if symbol.chars().nth(symbol.len() - 4).unwrap() == 'T' {\n            &symbol[(symbol.len() - 7)..(symbol.len() - 3)]\n        } else {\n            &symbol[(symbol.len() - 6)..(symbol.len() - 3)]\n        };\n        assert!(quote == \"USD\" || quote == \"USDT\");\n        assert_eq!(MarketType::QuantoFuture, get_market_type(symbol, EXCHANGE_NAME, None));\n    }\n}\n\n#[test]\n#[ignore = \"to avoid 429 Too Many Requests\"]\nfn fetch_linear_future_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::LinearFuture).unwrap();\n    assert!(!symbols.is_empty());\n    for symbol in symbols.iter() {\n        let date = &symbol[(symbol.len() - 2)..];\n        assert!(date.parse::<i64>().is_ok());\n        assert_eq!(MarketType::LinearFuture, get_market_type(symbol, EXCHANGE_NAME, None));\n    }\n}\n\n#[test]\nfn fetch_inverse_swap_markets() {\n    let markets = fetch_markets(EXCHANGE_NAME, MarketType::InverseSwap).unwrap();\n    assert!(!markets.is_empty());\n\n    let xbtusd = markets.iter().find(|m| m.symbol == \"XBTUSD\").unwrap();\n    assert_eq!(xbtusd.precision.tick_size, 0.5);\n    assert_eq!(xbtusd.precision.lot_size, 100.0);\n    assert_eq!(xbtusd.contract_value, Some(1.0));\n    assert!(xbtusd.quantity_limit.is_none());\n}\n\n#[test]\nfn test_contract_values() {\n    check_contract_values!(EXCHANGE_NAME, MarketType::Unknown);\n}\n"
  },
  {
    "path": "crypto-markets/tests/bitstamp.rs",
    "content": "use crypto_market_type::{get_market_types, MarketType};\nuse crypto_markets::{fetch_markets, fetch_symbols};\nuse crypto_pair::get_market_type;\n\n#[macro_use]\nmod utils;\n\nconst EXCHANGE_NAME: &str = \"bitstamp\";\n\n#[test]\nfn fetch_all_symbols() {\n    gen_all_symbols!();\n}\n\n#[test]\nfn fetch_spot_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::Spot).unwrap();\n    assert!(!symbols.is_empty());\n    for symbol in symbols.iter() {\n        assert_eq!(MarketType::Spot, get_market_type(symbol, EXCHANGE_NAME, None));\n    }\n}\n\n#[test]\nfn fetch_spot_markets() {\n    let markets = fetch_markets(EXCHANGE_NAME, MarketType::Spot).unwrap();\n    assert!(!markets.is_empty());\n\n    let btcusd = markets.iter().find(|m| m.symbol == \"btcusd\").unwrap().clone();\n    assert_eq!(btcusd.precision.tick_size, 0.00000001);\n    assert_eq!(btcusd.precision.lot_size, 1.0);\n    assert!(btcusd.quantity_limit.is_none());\n}\n"
  },
  {
    "path": "crypto-markets/tests/bitz.rs",
    "content": "use crypto_market_type::{get_market_types, MarketType};\nuse crypto_markets::fetch_symbols;\n\n#[macro_use]\nmod utils;\n\nconst EXCHANGE_NAME: &str = \"bitz\";\n\n#[test]\n#[ignore = \"bitz.com has shutdown since October 2021\"]\nfn fetch_all_symbols() {\n    gen_all_symbols!();\n}\n\n#[test]\n#[ignore = \"bitz.com has shutdown since October 2021\"]\nfn fetch_spot_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::Spot).unwrap();\n    assert!(!symbols.is_empty());\n    for symbol in symbols.iter() {\n        assert!(symbol.contains('_'));\n    }\n}\n\n#[test]\n#[ignore = \"bitz.com has shutdown since October 2021\"]\nfn fetch_inverse_swap_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::InverseSwap).unwrap();\n    assert!(!symbols.is_empty());\n    for symbol in symbols.iter() {\n        assert!(symbol.ends_with(\"_USD\"));\n    }\n}\n\n#[test]\n#[ignore = \"bitz.com has shutdown since October 2021\"]\nfn fetch_linear_swap_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::LinearSwap).unwrap();\n    assert!(!symbols.is_empty());\n    for symbol in symbols.iter() {\n        assert!(symbol.ends_with(\"_USDT\"));\n    }\n}\n"
  },
  {
    "path": "crypto-markets/tests/bybit.rs",
    "content": "use crypto_market_type::{get_market_types, MarketType};\nuse crypto_markets::{fetch_markets, fetch_symbols};\nuse crypto_pair::get_market_type;\nuse test_case::test_case;\n\n#[macro_use]\nmod utils;\n\nconst EXCHANGE_NAME: &str = \"bybit\";\n\n#[test]\nfn fetch_all_symbols() {\n    gen_all_symbols!();\n}\n\n#[test]\nfn fetch_inverse_swap_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::InverseSwap).unwrap();\n    assert!(!symbols.is_empty());\n    for symbol in symbols.iter() {\n        assert!(symbol.ends_with(\"USD\"));\n        assert_eq!(MarketType::InverseSwap, get_market_type(symbol, EXCHANGE_NAME, None));\n    }\n}\n\n#[test]\nfn fetch_linear_swap_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::LinearSwap).unwrap();\n    assert!(!symbols.is_empty());\n    for symbol in symbols.iter() {\n        assert!(symbol.ends_with(\"USDT\"));\n        assert_eq!(MarketType::LinearSwap, get_market_type(symbol, EXCHANGE_NAME, None));\n    }\n}\n\n#[test]\nfn fetch_inverse_future_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::InverseFuture).unwrap();\n    assert!(!symbols.is_empty());\n    for symbol in symbols.iter() {\n        let date = &symbol[(symbol.len() - 2)..];\n        assert!(date.parse::<i64>().is_ok());\n        assert_eq!(MarketType::InverseFuture, get_market_type(symbol, EXCHANGE_NAME, None));\n    }\n}\n\n#[test]\nfn fetch_inverse_swap_markets() {\n    let markets = fetch_markets(EXCHANGE_NAME, MarketType::InverseSwap).unwrap();\n    assert!(!markets.is_empty());\n    for market in markets.iter() {\n        assert!(market.delivery_date.is_none());\n    }\n\n    let btcusd = markets.iter().find(|m| m.symbol == \"BTCUSD\").unwrap().clone();\n    assert_eq!(btcusd.precision.tick_size, 0.5);\n    assert_eq!(btcusd.precision.lot_size, 1.0);\n    let quantity_limit = btcusd.quantity_limit.unwrap();\n    assert_eq!(quantity_limit.min.unwrap(), 1.0);\n    assert_eq!(quantity_limit.max, Some(1000000.0));\n}\n\n#[test]\nfn fetch_linear_swap_markets() {\n    let markets = fetch_markets(EXCHANGE_NAME, MarketType::LinearSwap).unwrap();\n    assert!(!markets.is_empty());\n    for market in markets.iter() {\n        assert!(market.delivery_date.is_none());\n    }\n\n    let btcusdt = markets.iter().find(|m| m.symbol == \"BTCUSDT\").unwrap().clone();\n    assert_eq!(btcusdt.precision.tick_size, 0.1);\n    assert_eq!(btcusdt.precision.lot_size, 0.001);\n    let quantity_limit = btcusdt.quantity_limit.unwrap();\n    assert_eq!(quantity_limit.min.unwrap(), 0.001);\n    assert_eq!(quantity_limit.max, Some(100.0));\n}\n\n#[test]\nfn fetch_inverse_future_markets() {\n    let markets = fetch_markets(EXCHANGE_NAME, MarketType::InverseFuture).unwrap();\n    assert!(!markets.is_empty());\n    for market in markets.iter() {\n        assert!(market.delivery_date.is_some());\n    }\n\n    let btcusd = markets.iter().find(|m| m.symbol.starts_with(\"BTCUSD\")).unwrap().clone();\n    assert_eq!(btcusd.precision.tick_size, 0.5);\n    assert_eq!(btcusd.precision.lot_size, 1.0);\n    let quantity_limit = btcusd.quantity_limit.unwrap();\n    assert_eq!(quantity_limit.min.unwrap(), 1.0);\n    assert_eq!(quantity_limit.max, Some(1000000.0));\n}\n\n#[test_case(MarketType::InverseFuture)]\n#[test_case(MarketType::InverseSwap)]\n#[test_case(MarketType::LinearSwap)]\nfn test_contract_values(market_type: MarketType) {\n    check_contract_values!(EXCHANGE_NAME, market_type);\n}\n"
  },
  {
    "path": "crypto-markets/tests/coinbase_pro.rs",
    "content": "use crypto_market_type::{get_market_types, MarketType};\nuse crypto_markets::{fetch_markets, fetch_symbols};\nuse crypto_pair::get_market_type;\n\n#[macro_use]\nmod utils;\n\nconst EXCHANGE_NAME: &str = \"coinbase_pro\";\n\n#[test]\nfn fetch_all_symbols() {\n    gen_all_symbols!();\n}\n\n#[test]\nfn fetch_spot_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::Spot).unwrap();\n    assert!(!symbols.is_empty());\n\n    for symbol in symbols.iter() {\n        assert!(symbol.contains('-'));\n        assert_eq!(symbol.to_string(), symbol.to_uppercase());\n        assert_eq!(MarketType::Spot, get_market_type(symbol, EXCHANGE_NAME, None));\n    }\n}\n\n#[test]\nfn fetch_spot_markets() {\n    let markets = fetch_markets(EXCHANGE_NAME, MarketType::Spot).unwrap();\n    assert!(!markets.is_empty());\n\n    let btcusd = markets.iter().find(|m| m.symbol == \"BTC-USD\").unwrap().clone();\n    assert_eq!(btcusd.precision.tick_size, 0.01);\n    assert_eq!(btcusd.precision.lot_size, 0.00000001);\n    let quantity_limit = btcusd.quantity_limit.unwrap();\n    assert_eq!(quantity_limit.min, None);\n    assert_eq!(quantity_limit.max, None);\n    assert_eq!(1.0, quantity_limit.notional_min.unwrap());\n}\n"
  },
  {
    "path": "crypto-markets/tests/deribit.rs",
    "content": "use crypto_market_type::MarketType;\nuse crypto_markets::{fetch_markets, fetch_symbols};\nuse crypto_pair::get_market_type;\nuse test_case::test_case;\n\n#[macro_use]\nmod utils;\n\nconst EXCHANGE_NAME: &str = \"deribit\";\n\n#[test]\nfn fetch_inverse_future_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::InverseFuture).unwrap();\n    assert!(!symbols.is_empty());\n\n    for symbol in symbols.iter() {\n        let year = &symbol[(symbol.len() - 2)..];\n        assert!(year.parse::<i64>().is_ok());\n\n        let date = &symbol[(symbol.len() - 7)..(symbol.len() - 5)];\n        assert!(date.parse::<i64>().is_ok());\n        assert_eq!(MarketType::InverseFuture, get_market_type(symbol, EXCHANGE_NAME, None));\n    }\n}\n\n#[test]\nfn fetch_inverse_swap_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::InverseSwap).unwrap();\n    assert!(!symbols.is_empty());\n\n    for symbol in symbols.iter() {\n        assert!(symbol.ends_with(\"-PERPETUAL\"));\n        assert_eq!(MarketType::InverseSwap, get_market_type(symbol, EXCHANGE_NAME, None));\n    }\n}\n\n#[test]\nfn fetch_option_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::EuropeanOption).unwrap();\n    assert!(!symbols.is_empty());\n\n    for symbol in symbols.iter() {\n        let arr: Vec<&str> = symbol.split('-').collect();\n\n        assert!(arr[0] == \"BTC\" || arr[0] == \"ETH\");\n\n        let date = &arr[1][..(arr[1].len() - 5)];\n        assert!(date.parse::<i64>().is_ok());\n\n        let year = &arr[1][(arr[1].len() - 2)..];\n        assert!(year.parse::<i64>().is_ok());\n\n        assert!(arr[2].parse::<i64>().is_ok());\n        assert!(arr[3] == \"C\" || arr[3] == \"P\");\n\n        assert_eq!(MarketType::EuropeanOption, get_market_type(symbol, EXCHANGE_NAME, None));\n    }\n}\n\n#[test]\nfn fetch_inverse_future_markets() {\n    let markets = fetch_markets(EXCHANGE_NAME, MarketType::InverseFuture).unwrap();\n    assert!(!markets.is_empty());\n\n    let btcusd = markets.iter().find(|m| m.base_id == \"BTC\").unwrap().clone();\n    assert_eq!(btcusd.contract_value, Some(10.0));\n    assert_eq!(btcusd.precision.tick_size, 2.5);\n    assert_eq!(btcusd.precision.lot_size, 10.0);\n    let quantity_limit = btcusd.quantity_limit.unwrap();\n    assert_eq!(quantity_limit.min.unwrap(), 10.0);\n    assert_eq!(quantity_limit.max, None);\n}\n\n#[test]\nfn fetch_inverse_swap_markets() {\n    let markets = fetch_markets(EXCHANGE_NAME, MarketType::InverseSwap).unwrap();\n    assert!(!markets.is_empty());\n\n    let btcusd = markets.iter().find(|m| m.base_id == \"BTC\").unwrap().clone();\n    assert_eq!(btcusd.contract_value, Some(10.0));\n    assert_eq!(btcusd.precision.tick_size, 0.5);\n    assert_eq!(btcusd.precision.lot_size, 10.0);\n    let quantity_limit = btcusd.quantity_limit.unwrap();\n    assert_eq!(quantity_limit.min.unwrap(), 10.0);\n    assert_eq!(quantity_limit.max, None);\n}\n\n#[test]\nfn fetch_option_markets() {\n    let markets = fetch_markets(EXCHANGE_NAME, MarketType::EuropeanOption).unwrap();\n    assert!(!markets.is_empty());\n\n    let btcusd = markets.iter().find(|m| m.base_id == \"BTC\").unwrap().clone();\n    assert_eq!(btcusd.contract_value, Some(1.0));\n    assert_eq!(btcusd.precision.tick_size, 0.0005);\n    assert_eq!(btcusd.precision.lot_size, 0.1);\n    let quantity_limit = btcusd.quantity_limit.unwrap();\n    assert_eq!(quantity_limit.min.unwrap(), 0.1);\n    assert_eq!(quantity_limit.max, None);\n}\n\n#[test_case(MarketType::InverseFuture)]\n#[test_case(MarketType::InverseSwap)]\n#[test_case(MarketType::EuropeanOption)]\nfn test_contract_values(market_type: MarketType) {\n    check_contract_values!(EXCHANGE_NAME, market_type);\n}\n"
  },
  {
    "path": "crypto-markets/tests/dydx.rs",
    "content": "use crypto_market_type::{get_market_types, MarketType};\nuse crypto_markets::{fetch_markets, fetch_symbols};\nuse crypto_pair::get_market_type;\nuse test_case::test_case;\n\n#[macro_use]\nmod utils;\n\nconst EXCHANGE_NAME: &str = \"dydx\";\n\n#[test]\nfn fetch_all_symbols() {\n    gen_all_symbols!();\n}\n\n#[test]\nfn fetch_linear_swap_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::LinearSwap).unwrap();\n    assert!(!symbols.is_empty());\n\n    for symbol in symbols.iter() {\n        assert!(symbol.ends_with(\"-USD\"));\n        assert_eq!(MarketType::LinearSwap, get_market_type(symbol, EXCHANGE_NAME, None));\n    }\n}\n\n#[test]\nfn fetch_linear_swap_markets() {\n    let markets = fetch_markets(EXCHANGE_NAME, MarketType::LinearSwap).unwrap();\n    assert!(!markets.is_empty());\n\n    let btcusd = markets.iter().find(|m| m.symbol == \"BTC-USD\").unwrap().clone();\n    assert_eq!(btcusd.precision.tick_size, 1.0);\n    assert_eq!(btcusd.precision.lot_size, 0.0001);\n    let quantity_limit = btcusd.quantity_limit.unwrap();\n    assert_eq!(quantity_limit.min.unwrap(), 0.001);\n    assert_eq!(quantity_limit.max, None);\n}\n\n#[test_case(MarketType::LinearSwap)]\nfn test_contract_values(market_type: MarketType) {\n    check_contract_values!(EXCHANGE_NAME, market_type);\n}\n"
  },
  {
    "path": "crypto-markets/tests/ftx.rs",
    "content": "use crypto_market_type::{get_market_types, MarketType};\nuse crypto_markets::{fetch_markets, fetch_symbols};\nuse crypto_pair::get_market_type;\n\n#[macro_use]\nmod utils;\n\nconst EXCHANGE_NAME: &str = \"ftx\";\n\n#[ignore = \"The FTX website is not operational.\"]\n#[test]\nfn fetch_all_symbols() {\n    gen_all_symbols!();\n}\n\n#[ignore = \"The FTX website is not operational.\"]\n#[test]\nfn fetch_spot_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::Spot).unwrap();\n    assert!(!symbols.is_empty());\n    for symbol in symbols.iter() {\n        assert!(symbol.contains('/'));\n        assert_eq!(*symbol, symbol.to_uppercase());\n        assert_eq!(MarketType::Spot, get_market_type(symbol, EXCHANGE_NAME, None));\n    }\n}\n\n#[ignore = \"The FTX website is not operational.\"]\n#[test]\nfn fetch_linear_swap_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::LinearSwap).unwrap();\n    assert!(!symbols.is_empty());\n    for symbol in symbols.iter() {\n        assert!(symbol.ends_with(\"-PERP\"));\n        assert_eq!(MarketType::LinearSwap, get_market_type(symbol, EXCHANGE_NAME, None));\n    }\n}\n\n#[ignore = \"The FTX website is not operational.\"]\n#[test]\nfn fetch_linear_future_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::LinearFuture).unwrap();\n    assert!(!symbols.is_empty());\n    for symbol in symbols.iter() {\n        let date = &symbol[(symbol.len() - 4)..];\n        assert!(date.parse::<i64>().is_ok());\n        assert_eq!(MarketType::LinearFuture, get_market_type(symbol, EXCHANGE_NAME, None));\n    }\n}\n\n#[ignore = \"The FTX website is not operational.\"]\n#[test]\nfn fetch_move_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::Move).unwrap();\n    assert!(!symbols.is_empty());\n    for symbol in symbols.iter() {\n        assert!(symbol.contains(\"-MOVE-\"));\n        assert_eq!(MarketType::Move, get_market_type(symbol, EXCHANGE_NAME, None));\n    }\n}\n\n#[ignore = \"The FTX website is not operational.\"]\n#[test]\nfn fetch_bvol_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::BVOL).unwrap();\n    assert!(!symbols.is_empty());\n    for symbol in symbols.iter() {\n        assert!(symbol.contains(\"BVOL/\"));\n        assert_eq!(MarketType::BVOL, get_market_type(symbol, EXCHANGE_NAME, None));\n    }\n}\n\n#[ignore = \"The FTX website is not operational.\"]\n#[test]\nfn fetch_spot_markets() {\n    let markets = fetch_markets(EXCHANGE_NAME, MarketType::Spot).unwrap();\n    assert!(!markets.is_empty());\n\n    let btcusdt = markets.iter().find(|m| m.symbol == \"BTC/USDT\").unwrap().clone();\n    assert!(btcusdt.contract_value.is_none());\n    assert_eq!(btcusdt.precision.tick_size, 1.0);\n    assert_eq!(btcusdt.precision.lot_size, 0.0001);\n    assert!(btcusdt.quantity_limit.is_none());\n}\n\n#[ignore = \"The FTX website is not operational.\"]\n#[test]\nfn fetch_linear_swap_markets() {\n    let markets = fetch_markets(EXCHANGE_NAME, MarketType::LinearSwap).unwrap();\n    assert!(!markets.is_empty());\n\n    let btcusd = markets.iter().find(|m| m.symbol == \"BTC-PERP\").unwrap().clone();\n    assert_eq!(btcusd.contract_value, Some(1.0));\n    assert_eq!(btcusd.precision.tick_size, 1.0);\n    assert_eq!(btcusd.precision.lot_size, 0.0001);\n    assert!(btcusd.quantity_limit.is_none());\n}\n\n#[ignore = \"The FTX website is not operational.\"]\n#[test]\nfn fetch_linear_future_markets() {\n    let markets = fetch_markets(EXCHANGE_NAME, MarketType::LinearFuture).unwrap();\n    assert!(!markets.is_empty());\n\n    let btcusd = markets.iter().find(|m| m.symbol.starts_with(\"BTC-\")).unwrap().clone();\n    assert_eq!(btcusd.contract_value, Some(1.0));\n    assert_eq!(btcusd.precision.tick_size, 1.0);\n    assert_eq!(btcusd.precision.lot_size, 0.0001);\n    assert!(btcusd.quantity_limit.is_none());\n    assert!(btcusd.delivery_date.is_some());\n}\n\n#[ignore = \"The FTX website is not operational.\"]\n#[test]\nfn fetch_move_markets() {\n    let markets = fetch_markets(EXCHANGE_NAME, MarketType::Move).unwrap();\n    assert!(!markets.is_empty());\n\n    let btcusd = markets.iter().find(|m| m.symbol.starts_with(\"BTC-MOVE-\")).unwrap().clone();\n    assert_eq!(btcusd.contract_value, Some(1.0));\n    assert_eq!(btcusd.precision.tick_size, 1.0);\n    assert_eq!(btcusd.precision.lot_size, 0.0001);\n    assert!(btcusd.quantity_limit.is_none());\n    assert!(btcusd.delivery_date.is_some());\n}\n\n#[ignore = \"The FTX website is not operational.\"]\n#[test]\nfn fetch_bvol_markets() {\n    let markets = fetch_markets(EXCHANGE_NAME, MarketType::BVOL).unwrap();\n    assert!(!markets.is_empty());\n\n    let btcusd = markets.iter().find(|m| m.symbol == \"BVOL/USD\").unwrap().clone();\n    assert_eq!(btcusd.contract_value, None);\n    assert_eq!(btcusd.precision.tick_size, 0.025);\n    assert_eq!(btcusd.precision.lot_size, 0.001);\n    assert!(btcusd.quantity_limit.is_none());\n    assert!(btcusd.delivery_date.is_none());\n}\n\n// The FTX website is not operational\n// #[test_case(MarketType::LinearSwap)]\n// #[test_case(MarketType::LinearFuture)]\n// fn test_contract_values(market_type: MarketType) {\n//     check_contract_values!(EXCHANGE_NAME, market_type);\n// }\n"
  },
  {
    "path": "crypto-markets/tests/gate.rs",
    "content": "use crypto_market_type::{get_market_types, MarketType};\nuse crypto_markets::{fetch_markets, fetch_symbols};\nuse test_case::test_case;\n\n#[macro_use]\nmod utils;\n\nconst EXCHANGE_NAME: &str = \"gate\";\n\n#[test]\nfn fetch_all_symbols() {\n    gen_all_symbols!();\n}\n\n#[test]\nfn fetch_spot_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::Spot).unwrap();\n    assert!(!symbols.is_empty());\n    for symbol in symbols.iter() {\n        if symbol.ends_with(\"_USD\") {\n            println!(\"{symbol}\");\n        }\n\n        assert!(symbol.contains('_'));\n        assert_eq!(symbol.to_string(), symbol.to_uppercase());\n    }\n}\n\n#[test]\nfn fetch_inverse_swap_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::InverseSwap).unwrap();\n    assert!(!symbols.is_empty());\n    for symbol in symbols.iter() {\n        assert!(symbol.ends_with(\"_USD\"));\n    }\n}\n\n#[test]\nfn fetch_linear_swap_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::LinearSwap).unwrap();\n    assert!(!symbols.is_empty());\n    for symbol in symbols.iter() {\n        assert!(symbol.ends_with(\"_USDT\"));\n    }\n}\n\n#[test]\nfn fetch_inverse_future_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::InverseFuture).unwrap();\n    for symbol in symbols.iter() {\n        let date = &symbol[(symbol.len() - 8)..];\n        assert!(date.parse::<i64>().is_ok());\n        assert!(symbol.contains(\"_USD_\"));\n    }\n}\n\n#[test]\nfn fetch_linear_future_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::LinearFuture).unwrap();\n    assert!(!symbols.is_empty());\n    for symbol in symbols.iter() {\n        let date = &symbol[(symbol.len() - 8)..];\n        assert!(date.parse::<i64>().is_ok());\n        assert!(symbol.contains(\"_USDT_\"));\n    }\n}\n\n#[test]\nfn fetch_spot_markets() {\n    let markets = fetch_markets(EXCHANGE_NAME, MarketType::Spot).unwrap();\n    assert!(!markets.is_empty());\n\n    let btc_usdt = markets.iter().find(|m| m.symbol == \"BTC_USDT\").unwrap().clone();\n    assert_eq!(btc_usdt.market_type, MarketType::Spot);\n    assert!(btc_usdt.contract_value.is_none());\n    assert_eq!(btc_usdt.precision.tick_size, 0.1);\n    assert_eq!(btc_usdt.precision.lot_size, 0.0001);\n    let quantity_limit = btc_usdt.quantity_limit.unwrap();\n    assert_eq!(quantity_limit.min.unwrap(), 0.0001);\n    assert!(quantity_limit.max.is_none());\n}\n\n#[test]\nfn fetch_inverse_swap_markets() {\n    let markets = fetch_markets(EXCHANGE_NAME, MarketType::InverseSwap).unwrap();\n    assert!(!markets.is_empty());\n\n    let btc_usd = markets.iter().find(|m| m.symbol == \"BTC_USD\").unwrap().clone();\n    assert_eq!(btc_usd.market_type, MarketType::InverseSwap);\n    assert_eq!(btc_usd.contract_value, Some(1.0));\n    assert_eq!(btc_usd.precision.tick_size, 0.1);\n    assert_eq!(btc_usd.precision.lot_size, 1.0);\n    let quantity_limit = btc_usd.quantity_limit.unwrap();\n    assert_eq!(quantity_limit.min.unwrap(), 1.0);\n    assert_eq!(quantity_limit.max, Some(1000000.0));\n}\n\n#[test]\nfn fetch_linear_swap_markets() {\n    let markets = fetch_markets(EXCHANGE_NAME, MarketType::LinearSwap).unwrap();\n    assert!(!markets.is_empty());\n\n    let btc_usdt = markets.iter().find(|m| m.symbol == \"BTC_USDT\").unwrap().clone();\n    assert_eq!(btc_usdt.market_type, MarketType::LinearSwap);\n    assert_eq!(btc_usdt.contract_value, Some(0.0001));\n    assert_eq!(btc_usdt.precision.tick_size, 0.1);\n    assert_eq!(btc_usdt.precision.lot_size, 0.0001);\n    let quantity_limit = btc_usdt.quantity_limit.unwrap();\n    assert_eq!(quantity_limit.min.unwrap(), 1.0);\n    assert_eq!(quantity_limit.max, Some(1000000.0));\n}\n\n#[ignore = \"Gate inverse future market has no trading symbols\"]\n#[test]\nfn fetch_inverse_future_markets() {\n    let markets = fetch_markets(EXCHANGE_NAME, MarketType::InverseFuture).unwrap();\n    assert!(!markets.is_empty());\n\n    let btc_usd = markets.iter().find(|m| m.symbol.starts_with(\"BTC_USD_\")).unwrap().clone();\n    assert_eq!(btc_usd.market_type, MarketType::InverseFuture);\n    assert_eq!(btc_usd.contract_value, Some(1.0));\n    assert!(btc_usd.delivery_date.is_some());\n    assert_eq!(btc_usd.precision.tick_size, 0.1);\n    assert_eq!(btc_usd.precision.lot_size, 1.0);\n    let quantity_limit = btc_usd.quantity_limit.unwrap();\n    assert_eq!(quantity_limit.min.unwrap(), 1.0);\n    assert_eq!(quantity_limit.max, Some(1000000.0));\n}\n\n#[test]\nfn fetch_linear_future_markets() {\n    let markets = fetch_markets(EXCHANGE_NAME, MarketType::LinearFuture).unwrap();\n    assert!(!markets.is_empty());\n\n    let btc_usdt = markets.iter().find(|m| m.symbol.starts_with(\"BTC_USDT_\")).unwrap().clone();\n    assert_eq!(btc_usdt.market_type, MarketType::LinearFuture);\n    assert_eq!(btc_usdt.contract_value, Some(0.0001));\n    assert!(btc_usdt.delivery_date.is_some());\n    assert_eq!(btc_usdt.precision.tick_size, 0.1);\n    assert_eq!(btc_usdt.precision.lot_size, 0.0001);\n    let quantity_limit = btc_usdt.quantity_limit.unwrap();\n    assert_eq!(quantity_limit.min.unwrap(), 1.0);\n    assert_eq!(quantity_limit.max, Some(1000000.0));\n}\n\n#[test_case(MarketType::LinearFuture)]\n#[test_case(MarketType::LinearSwap)]\n// TODO: ETH_USD is actually a quanto swap contract\n// #[test_case(MarketType::InverseFuture)]\n// #[test_case(MarketType::InverseSwap)]\nfn test_contract_values(market_type: MarketType) {\n    check_contract_values!(EXCHANGE_NAME, market_type);\n}\n"
  },
  {
    "path": "crypto-markets/tests/huobi.rs",
    "content": "use crypto_market_type::{get_market_types, MarketType};\nuse crypto_markets::{fetch_markets, fetch_symbols};\nuse crypto_pair::get_market_type;\nuse test_case::test_case;\n\n#[macro_use]\nmod utils;\n\nconst EXCHANGE_NAME: &str = \"huobi\";\n\n#[test]\nfn fetch_all_symbols() {\n    gen_all_symbols!();\n}\n\n#[test]\nfn fetch_spot_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::Spot).unwrap();\n    assert!(!symbols.is_empty());\n    for symbol in symbols.iter() {\n        assert_eq!(symbol.to_lowercase(), symbol.to_string());\n        assert_eq!(MarketType::Spot, get_market_type(symbol, EXCHANGE_NAME, None));\n    }\n}\n\n#[test]\nfn fetch_inverse_future_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::InverseFuture).unwrap();\n    assert!(!symbols.is_empty());\n    for symbol in symbols.iter() {\n        assert!(\n            symbol.ends_with(\"_CW\")\n                || symbol.ends_with(\"_NW\")\n                || symbol.ends_with(\"_CQ\")\n                || symbol.ends_with(\"_NQ\")\n        );\n        assert_eq!(MarketType::InverseFuture, get_market_type(symbol, EXCHANGE_NAME, None));\n    }\n}\n\n#[test]\nfn fetch_inverse_swap_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::InverseSwap).unwrap();\n    assert!(!symbols.is_empty());\n    for symbol in symbols.iter() {\n        assert!(symbol.ends_with(\"-USD\"));\n        assert_eq!(MarketType::InverseSwap, get_market_type(symbol, EXCHANGE_NAME, None));\n    }\n}\n\n#[test]\nfn fetch_linear_swap_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::LinearSwap).unwrap();\n    assert!(!symbols.is_empty());\n    for symbol in symbols.iter() {\n        assert!(symbol.ends_with(\"-USDT\"));\n        assert_eq!(MarketType::LinearSwap, get_market_type(symbol, EXCHANGE_NAME, None));\n    }\n}\n\n#[test]\n#[ignore]\nfn fetch_option_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::EuropeanOption).unwrap();\n    assert!(!symbols.is_empty());\n    for symbol in symbols.iter() {\n        assert!(symbol.contains(\"-C-\") || symbol.contains(\"-P-\"));\n        assert_eq!(MarketType::EuropeanOption, get_market_type(symbol, EXCHANGE_NAME, None));\n    }\n}\n\n#[test]\nfn fetch_spot_markets() {\n    let markets = fetch_markets(EXCHANGE_NAME, MarketType::Spot).unwrap();\n    assert!(!markets.is_empty());\n\n    let btc_usdt = markets.iter().find(|m| m.symbol == \"btcusdt\").unwrap().clone();\n    assert_eq!(btc_usdt.precision.tick_size, 0.01);\n    assert_eq!(btc_usdt.precision.lot_size, 0.000001);\n    let quantity_limit = btc_usdt.quantity_limit.unwrap();\n    assert_eq!(quantity_limit.min.unwrap(), 0.0001);\n    assert_eq!(quantity_limit.max, Some(1000.0));\n}\n\n#[test]\nfn fetch_inverse_future_markets() {\n    let markets = fetch_markets(EXCHANGE_NAME, MarketType::InverseFuture).unwrap();\n    assert!(!markets.is_empty());\n\n    let btc_usd = markets.iter().find(|m| m.symbol == \"BTC_CW\").unwrap().clone();\n    assert_eq!(btc_usd.precision.tick_size, 0.01);\n    assert_eq!(btc_usd.precision.lot_size, 1.0);\n    assert!(btc_usd.quantity_limit.is_none());\n}\n\n#[test]\nfn fetch_inverse_swap_markets() {\n    let markets = fetch_markets(EXCHANGE_NAME, MarketType::InverseSwap).unwrap();\n    assert!(!markets.is_empty());\n\n    let btc_usd = markets.iter().find(|m| m.symbol == \"BTC-USD\").unwrap().clone();\n    assert_eq!(btc_usd.precision.tick_size, 0.1);\n    assert_eq!(btc_usd.precision.lot_size, 1.0);\n    assert!(btc_usd.quantity_limit.is_none());\n}\n\n#[test]\nfn fetch_linear_swap_markets() {\n    let markets = fetch_markets(EXCHANGE_NAME, MarketType::LinearSwap).unwrap();\n    assert!(!markets.is_empty());\n\n    let btc_usdt = markets.iter().find(|m| m.symbol == \"BTC-USDT\").unwrap().clone();\n    assert_eq!(btc_usdt.precision.tick_size, 0.1);\n    assert_eq!(btc_usdt.precision.lot_size, 1.0);\n    assert!(btc_usdt.quantity_limit.is_none());\n}\n\n#[test_case(MarketType::InverseFuture)]\n#[test_case(MarketType::InverseSwap)]\n#[test_case(MarketType::LinearSwap)]\nfn test_contract_values(market_type: MarketType) {\n    check_contract_values!(EXCHANGE_NAME, market_type);\n}\n"
  },
  {
    "path": "crypto-markets/tests/kraken.rs",
    "content": "use crypto_market_type::{get_market_types, MarketType};\nuse crypto_markets::{fetch_markets, fetch_symbols};\nuse crypto_pair::get_market_type;\n\n#[macro_use]\nmod utils;\n\nconst EXCHANGE_NAME: &str = \"kraken\";\n\n#[test]\nfn fetch_all_symbols() {\n    gen_all_symbols!();\n}\n\n#[test]\nfn fetch_spot_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::Spot).unwrap();\n    assert!(!symbols.is_empty());\n    for symbol in symbols.iter() {\n        assert!(symbol.contains('/'));\n        assert_eq!(MarketType::Spot, get_market_type(symbol, EXCHANGE_NAME, None));\n    }\n}\n\n#[test]\nfn fetch_inverse_future_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::InverseFuture).unwrap();\n    assert!(!symbols.is_empty());\n    for symbol in symbols.iter() {\n        assert!(symbol.starts_with(\"FI_\"));\n        let date = &symbol[(symbol.len() - 6)..];\n        assert!(date.parse::<i64>().is_ok());\n        assert_eq!(MarketType::InverseFuture, get_market_type(symbol, EXCHANGE_NAME, None));\n    }\n}\n\n#[test]\nfn fetch_inverse_swap_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::InverseSwap).unwrap();\n    assert!(!symbols.is_empty());\n    for symbol in symbols.iter() {\n        assert!(symbol.starts_with(\"PI_\"));\n        assert!(symbol.ends_with(\"USD\"));\n        assert_eq!(MarketType::InverseSwap, get_market_type(symbol, EXCHANGE_NAME, None));\n    }\n}\n\n#[test]\nfn fetch_spot_markets() {\n    let markets = fetch_markets(EXCHANGE_NAME, MarketType::Spot).unwrap();\n    assert!(!markets.is_empty());\n\n    let btcusd = markets.iter().find(|m| m.symbol == \"XBT/USD\").unwrap().clone();\n    assert_eq!(btcusd.precision.tick_size, 0.1);\n    assert_eq!(btcusd.precision.lot_size, 0.00000001);\n    let quantity_limit = btcusd.quantity_limit.unwrap();\n    assert_eq!(quantity_limit.min.unwrap(), 0.0001);\n    assert_eq!(quantity_limit.max, None);\n}\n\n#[test]\nfn fetch_inverse_future_markets() {\n    let markets = fetch_markets(EXCHANGE_NAME, MarketType::InverseFuture).unwrap();\n    assert!(!markets.is_empty());\n\n    let btcusd = markets.iter().find(|m| m.symbol.starts_with(\"fi_xbtusd_\")).unwrap().clone();\n    assert_eq!(btcusd.precision.tick_size, 0.5);\n    assert_eq!(btcusd.precision.lot_size, 1.0);\n    let quantity_limit = btcusd.quantity_limit.unwrap();\n    assert_eq!(quantity_limit.min.unwrap(), 1.0);\n    assert_eq!(quantity_limit.max, None);\n}\n\n#[test]\nfn fetch_inverse_swap_markets() {\n    let markets = fetch_markets(EXCHANGE_NAME, MarketType::InverseSwap).unwrap();\n    assert!(!markets.is_empty());\n\n    let btcusd = markets.iter().find(|m| m.symbol == \"pi_xbtusd\").unwrap().clone();\n    assert_eq!(btcusd.precision.tick_size, 0.5);\n    assert_eq!(btcusd.precision.lot_size, 1.0);\n    let quantity_limit = btcusd.quantity_limit.unwrap();\n    assert_eq!(quantity_limit.min.unwrap(), 1.0);\n    assert_eq!(quantity_limit.max, None);\n}\n"
  },
  {
    "path": "crypto-markets/tests/kucoin.rs",
    "content": "use crypto_market_type::{get_market_types, MarketType};\nuse crypto_markets::{fetch_markets, fetch_symbols};\nuse crypto_pair::get_market_type;\nuse test_case::test_case;\n\n#[macro_use]\nmod utils;\n\nconst EXCHANGE_NAME: &str = \"kucoin\";\n\n#[test]\nfn fetch_all_symbols() {\n    gen_all_symbols!();\n}\n\n#[test]\nfn fetch_spot_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::Spot).unwrap();\n    assert!(!symbols.is_empty());\n    for symbol in symbols.iter() {\n        assert!(symbol.contains('-'));\n        assert_eq!(symbol.to_string(), symbol.to_uppercase());\n        assert_eq!(MarketType::Spot, get_market_type(symbol, EXCHANGE_NAME, None));\n    }\n}\n\n#[test]\nfn fetch_inverse_swap_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::InverseSwap).unwrap();\n    assert!(!symbols.is_empty());\n    for symbol in symbols.iter() {\n        assert!(symbol.ends_with(\"USDM\"));\n        assert_eq!(MarketType::InverseSwap, get_market_type(symbol, EXCHANGE_NAME, None));\n    }\n}\n\n#[test]\nfn fetch_linear_swap_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::LinearSwap).unwrap();\n    assert!(!symbols.is_empty());\n    for symbol in symbols.iter() {\n        assert!(symbol.ends_with(\"USDTM\") || symbol.ends_with(\"USDCM\"));\n        assert_eq!(MarketType::LinearSwap, get_market_type(symbol, EXCHANGE_NAME, None));\n    }\n}\n\n#[test]\nfn fetch_inverse_future_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::InverseFuture).unwrap();\n    assert!(!symbols.is_empty());\n    for symbol in symbols.iter() {\n        let date = &symbol[(symbol.len() - 2)..];\n        assert!(date.parse::<i64>().is_ok());\n    }\n}\n\n#[test]\nfn fetch_spot_markets() {\n    let markets = fetch_markets(EXCHANGE_NAME, MarketType::Spot).unwrap();\n    assert!(!markets.is_empty());\n\n    let btc_usdt = markets.iter().find(|m| m.symbol == \"BTC-USDT\").unwrap().clone();\n    assert_eq!(btc_usdt.market_type, MarketType::Spot);\n    assert!(btc_usdt.contract_value.is_none());\n    assert_eq!(btc_usdt.precision.tick_size, 0.1);\n    assert_eq!(btc_usdt.precision.lot_size, 0.00000001);\n    let quantity_limit = btc_usdt.quantity_limit.unwrap();\n    assert_eq!(quantity_limit.min.unwrap(), 0.00001);\n    assert_eq!(quantity_limit.max, Some(10000000000.0));\n}\n\n#[test]\nfn fetch_inverse_future_markets() {\n    let markets = fetch_markets(EXCHANGE_NAME, MarketType::InverseFuture).unwrap();\n    assert!(!markets.is_empty());\n\n    let btcusd = markets.iter().find(|m| m.base == \"BTC\" && m.quote == \"USD\").unwrap().clone();\n    assert_eq!(btcusd.market_type, MarketType::InverseFuture);\n    assert_eq!(btcusd.contract_value, Some(1.0));\n    assert_eq!(btcusd.precision.tick_size, 1.0);\n    assert_eq!(btcusd.precision.lot_size, 1.0);\n    assert!(btcusd.quantity_limit.is_none());\n}\n\n#[test]\nfn fetch_inverse_swap_markets() {\n    let markets = fetch_markets(EXCHANGE_NAME, MarketType::InverseSwap).unwrap();\n    assert!(!markets.is_empty());\n\n    let btcusd_perp = markets.iter().find(|m| m.symbol == \"XBTUSDM\").unwrap().clone();\n    assert_eq!(btcusd_perp.market_type, MarketType::InverseSwap);\n    assert_eq!(btcusd_perp.contract_value, Some(1.0));\n    assert_eq!(btcusd_perp.precision.tick_size, 1.0);\n    assert_eq!(btcusd_perp.precision.lot_size, 1.0);\n    assert!(btcusd_perp.quantity_limit.is_none());\n}\n\n#[test]\nfn fetch_linear_swap_markets() {\n    let markets = fetch_markets(EXCHANGE_NAME, MarketType::LinearSwap).unwrap();\n    assert!(!markets.is_empty());\n\n    let btcusdt = markets.iter().find(|m| m.symbol == \"XBTUSDTM\").unwrap().clone();\n    assert_eq!(btcusdt.market_type, MarketType::LinearSwap);\n    assert_eq!(btcusdt.contract_value, Some(0.001));\n    assert_eq!(btcusdt.precision.tick_size, 1.0);\n    assert_eq!(btcusdt.precision.lot_size, 1.0);\n    assert!(btcusdt.quantity_limit.is_none());\n}\n\n#[test_case(MarketType::InverseFuture)]\n#[test_case(MarketType::InverseSwap)]\n#[test_case(MarketType::LinearSwap)]\nfn test_contract_values(market_type: MarketType) {\n    check_contract_values!(EXCHANGE_NAME, market_type);\n}\n"
  },
  {
    "path": "crypto-markets/tests/mexc.rs",
    "content": "use crypto_market_type::MarketType;\nuse crypto_markets::{fetch_markets, fetch_symbols};\nuse crypto_pair::get_market_type;\nuse test_case::test_case;\n\n#[macro_use]\nmod utils;\n\nconst EXCHANGE_NAME: &str = \"mexc\";\n\n#[test]\nfn fetch_spot_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::Spot).unwrap();\n    assert!(!symbols.is_empty());\n\n    for symbol in symbols.iter() {\n        assert!(symbol.contains('_'));\n        assert_eq!(symbol.to_uppercase(), symbol.to_string());\n        assert_eq!(MarketType::Spot, get_market_type(symbol, EXCHANGE_NAME, Some(true)));\n    }\n}\n\n#[test]\nfn fetch_linear_swap_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::LinearSwap).unwrap();\n    assert!(!symbols.is_empty());\n    for symbol in symbols.iter() {\n        assert!(symbol.ends_with(\"_USDT\"));\n        assert_eq!(MarketType::LinearSwap, get_market_type(symbol, EXCHANGE_NAME, None));\n    }\n}\n\n#[test]\nfn fetch_inverse_swap_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::InverseSwap).unwrap();\n    assert!(!symbols.is_empty());\n    for symbol in symbols.iter() {\n        assert!(symbol.ends_with(\"_USD\"));\n        assert_eq!(MarketType::InverseSwap, get_market_type(symbol, EXCHANGE_NAME, None));\n    }\n}\n\n#[test]\nfn fetch_spot_markets() {\n    let markets = fetch_markets(EXCHANGE_NAME, MarketType::Spot).unwrap();\n    assert!(!markets.is_empty());\n\n    let btc_usdt = markets.iter().find(|m| m.symbol == \"BTC_USDT\").unwrap().clone();\n    assert_eq!(btc_usdt.market_type, MarketType::Spot);\n    assert!(btc_usdt.contract_value.is_none());\n    assert_eq!(btc_usdt.precision.tick_size, 0.01);\n    assert_eq!(btc_usdt.precision.lot_size, 0.000001);\n    let quantity_limit = btc_usdt.quantity_limit.unwrap();\n    assert_eq!(quantity_limit.min.unwrap(), 5.0);\n    assert_eq!(quantity_limit.max, Some(5000000.0));\n}\n\n#[test]\nfn fetch_inverse_swap_markets() {\n    let markets = fetch_markets(EXCHANGE_NAME, MarketType::InverseSwap).unwrap();\n    assert!(!markets.is_empty());\n\n    let btcusd_perp = markets.iter().find(|m| m.symbol == \"BTC_USD\").unwrap().clone();\n    assert_eq!(btcusd_perp.market_type, MarketType::InverseSwap);\n    assert_eq!(btcusd_perp.contract_value, Some(100.0));\n    assert_eq!(btcusd_perp.precision.tick_size, 0.1);\n    assert_eq!(btcusd_perp.precision.lot_size, 1.0);\n    let quantity_limit = btcusd_perp.quantity_limit.unwrap();\n    assert_eq!(quantity_limit.min.unwrap(), 1.0);\n    assert_eq!(quantity_limit.max, Some(30000.0));\n}\n\n#[test]\nfn fetch_linear_swap_markets() {\n    let markets = fetch_markets(EXCHANGE_NAME, MarketType::LinearSwap).unwrap();\n    assert!(!markets.is_empty());\n\n    let btcusdt = markets.iter().find(|m| m.symbol == \"BTC_USDT\").unwrap().clone();\n    assert_eq!(btcusdt.market_type, MarketType::LinearSwap);\n    assert_eq!(btcusdt.contract_value, Some(0.0001));\n    assert_eq!(btcusdt.precision.tick_size, 0.1);\n    assert_eq!(btcusdt.precision.lot_size, 1.0);\n    let quantity_limit = btcusdt.quantity_limit.unwrap();\n    assert_eq!(quantity_limit.min.unwrap(), 1.0);\n    assert_eq!(quantity_limit.max, Some(2625000.0));\n}\n\n#[test_case(MarketType::InverseSwap)]\n#[test_case(MarketType::LinearSwap)]\nfn test_contract_values(market_type: MarketType) {\n    check_contract_values!(EXCHANGE_NAME, market_type);\n}\n"
  },
  {
    "path": "crypto-markets/tests/okx.rs",
    "content": "use crypto_market_type::{get_market_types, MarketType};\nuse crypto_markets::{fetch_markets, fetch_symbols};\nuse crypto_pair::get_market_type;\nuse test_case::test_case;\n\n#[macro_use]\nmod utils;\n\nconst EXCHANGE_NAME: &str = \"okx\";\n\n#[test]\nfn fetch_all_symbols() {\n    gen_all_symbols!();\n}\n\n#[test]\nfn fetch_spot_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::Spot).unwrap();\n    assert!(!symbols.is_empty());\n    for symbol in symbols.iter() {\n        assert!(symbol.contains('-'));\n        assert_eq!(MarketType::Spot, get_market_type(symbol, EXCHANGE_NAME, None));\n    }\n}\n\n#[test]\nfn fetch_inverse_future_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::InverseFuture).unwrap();\n    assert!(!symbols.is_empty());\n    for symbol in symbols.iter() {\n        let date = &symbol[(symbol.len() - 6)..];\n        assert!(date.parse::<i64>().is_ok());\n\n        assert!(symbol.contains(\"-USD-\"));\n        assert_eq!(MarketType::InverseFuture, get_market_type(symbol, EXCHANGE_NAME, None));\n    }\n}\n\n#[test]\nfn fetch_linear_future_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::LinearFuture).unwrap();\n    assert!(!symbols.is_empty());\n    for symbol in symbols.iter() {\n        let date = &symbol[(symbol.len() - 6)..];\n        assert!(date.parse::<i64>().is_ok());\n\n        assert!(symbol.contains(\"-USDT-\") || symbol.contains(\"-USDC-\"));\n        assert_eq!(MarketType::LinearFuture, get_market_type(symbol, EXCHANGE_NAME, None));\n    }\n}\n\n#[test]\nfn fetch_inverse_swap_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::InverseSwap).unwrap();\n    assert!(!symbols.is_empty());\n    for symbol in symbols.iter() {\n        assert!(symbol.ends_with(\"-USD-SWAP\"));\n        assert_eq!(MarketType::InverseSwap, get_market_type(symbol, EXCHANGE_NAME, None));\n    }\n}\n\n#[test]\nfn fetch_linear_swap_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::LinearSwap).unwrap();\n    assert!(!symbols.is_empty());\n    for symbol in symbols.iter() {\n        assert!(symbol.ends_with(\"-USDT-SWAP\") || symbol.ends_with(\"-USDC-SWAP\"));\n        assert_eq!(MarketType::LinearSwap, get_market_type(symbol, EXCHANGE_NAME, None));\n    }\n}\n\n#[test]\nfn fetch_option_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::EuropeanOption).unwrap();\n    assert!(!symbols.is_empty());\n    for symbol in symbols.iter() {\n        assert!(symbol.ends_with(\"-C\") || symbol.ends_with(\"-P\"));\n        assert_eq!(MarketType::EuropeanOption, get_market_type(symbol, EXCHANGE_NAME, None));\n    }\n}\n\n#[test]\nfn fetch_spot_markets() {\n    let markets = fetch_markets(EXCHANGE_NAME, MarketType::Spot).unwrap();\n    assert!(!markets.is_empty());\n\n    let btc_usdt = markets.iter().find(|m| m.symbol == \"BTC-USDT\").unwrap().clone();\n    assert_eq!(btc_usdt.precision.tick_size, 0.1);\n    assert_eq!(btc_usdt.precision.lot_size, 0.00000001);\n    let quantity_limit = btc_usdt.quantity_limit.unwrap();\n    assert_eq!(quantity_limit.min.unwrap(), 0.00001);\n    assert_eq!(quantity_limit.max, None);\n}\n\n#[test]\nfn fetch_inverse_future_markets() {\n    let markets = fetch_markets(EXCHANGE_NAME, MarketType::InverseFuture).unwrap();\n    assert!(!markets.is_empty());\n\n    let btc_usd = markets.iter().find(|m| m.symbol.starts_with(\"BTC-USD-\")).unwrap().clone();\n    assert_eq!(btc_usd.precision.tick_size, 0.1);\n    assert_eq!(btc_usd.precision.lot_size, 1.0);\n    let quantity_limit = btc_usd.quantity_limit.unwrap();\n    assert_eq!(quantity_limit.min.unwrap(), 1.0);\n    assert_eq!(quantity_limit.max, None);\n}\n\n#[test]\nfn fetch_linear_future_markets() {\n    let markets = fetch_markets(EXCHANGE_NAME, MarketType::LinearFuture).unwrap();\n    assert!(!markets.is_empty());\n\n    let btc_usdt = markets.iter().find(|m| m.symbol.starts_with(\"BTC-USDT-\")).unwrap().clone();\n    assert_eq!(btc_usdt.precision.tick_size, 0.1);\n    assert_eq!(btc_usdt.precision.lot_size, 1.0);\n    let quantity_limit = btc_usdt.quantity_limit.unwrap();\n    assert_eq!(quantity_limit.min.unwrap(), 1.0);\n    assert_eq!(quantity_limit.max, None);\n}\n\n#[test]\nfn fetch_inverse_swap_markets() {\n    let markets = fetch_markets(EXCHANGE_NAME, MarketType::InverseSwap).unwrap();\n    assert!(!markets.is_empty());\n\n    let btc_usd = markets.iter().find(|m| m.symbol == \"BTC-USD-SWAP\").unwrap().clone();\n    assert_eq!(btc_usd.precision.tick_size, 0.1);\n    assert_eq!(btc_usd.precision.lot_size, 1.0);\n    let quantity_limit = btc_usd.quantity_limit.unwrap();\n    assert_eq!(quantity_limit.min.unwrap(), 1.0);\n    assert_eq!(quantity_limit.max, None);\n}\n\n#[test]\nfn fetch_linear_swap_markets() {\n    let markets = fetch_markets(EXCHANGE_NAME, MarketType::LinearSwap).unwrap();\n    assert!(!markets.is_empty());\n\n    let btc_usdt = markets.iter().find(|m| m.symbol == \"BTC-USDT-SWAP\").unwrap().clone();\n    assert_eq!(btc_usdt.precision.tick_size, 0.1);\n    assert_eq!(btc_usdt.precision.lot_size, 1.0);\n    let quantity_limit = btc_usdt.quantity_limit.unwrap();\n    assert_eq!(quantity_limit.min.unwrap(), 1.0);\n    assert_eq!(quantity_limit.max, None);\n}\n\n#[test]\nfn fetch_option_markets() {\n    let markets = fetch_markets(EXCHANGE_NAME, MarketType::EuropeanOption).unwrap();\n    assert!(!markets.is_empty());\n\n    let btc_usd = markets.iter().find(|m| m.symbol.starts_with(\"BTC-USD-\")).unwrap().clone();\n    assert_eq!(btc_usd.precision.tick_size, 0.0005);\n    assert_eq!(btc_usd.precision.lot_size, 1.0);\n    let quantity_limit = btc_usd.quantity_limit.unwrap();\n    assert_eq!(quantity_limit.min.unwrap(), 1.0);\n    assert_eq!(quantity_limit.max, None);\n}\n\n// #[test_case(MarketType::InverseFuture)]\n// #[test_case(MarketType::LinearFuture)]\n// #[test_case(MarketType::InverseSwap)]\n// #[test_case(MarketType::LinearSwap)]\n#[test_case(MarketType::EuropeanOption)]\nfn test_contract_values(market_type: MarketType) {\n    check_contract_values!(EXCHANGE_NAME, market_type);\n}\n"
  },
  {
    "path": "crypto-markets/tests/utils/mod.rs",
    "content": "#[allow(unused_macros)]\nmacro_rules! gen_all_symbols {\n    () => {\n        let market_types = get_market_types(EXCHANGE_NAME);\n        assert!(!market_types.is_empty());\n\n        for market_type in market_types.into_iter().filter(|m| m != &MarketType::Unknown) {\n            let symbols = fetch_symbols(EXCHANGE_NAME, market_type).unwrap();\n            if EXCHANGE_NAME != \"gate\" && market_type != MarketType::InverseFuture {\n                assert!(!symbols.is_empty());\n            }\n        }\n    };\n}\n\n#[allow(unused_macros)]\nmacro_rules! check_contract_values {\n    ($exchange:expr, $market_type:expr) => {{\n        let markets = fetch_markets($exchange, $market_type).unwrap();\n        for market in markets.into_iter().filter(|m| {\n            m.market_type == MarketType::InverseSwap\n                || m.market_type == MarketType::LinearSwap\n                || m.market_type == MarketType::InverseFuture\n                || m.market_type == MarketType::LinearFuture\n        }) {\n            let contract_value = crypto_contract_value::get_contract_value(\n                &market.exchange,\n                market.market_type,\n                format!(\"{}/{}\", market.base, market.quote).as_str(),\n            );\n            assert_eq!(market.contract_value, contract_value);\n            if market.base != crypto_pair::normalize_currency(market.base_id.as_str(), $exchange) {\n                println!(\"{}\", serde_json::to_string(&market).unwrap());\n            }\n            assert_eq!(\n                market.base,\n                crypto_pair::normalize_currency(market.base_id.as_str(), $exchange)\n            );\n            assert_eq!(\n                market.quote,\n                crypto_pair::normalize_currency(market.quote_id.as_str(), $exchange)\n            );\n            assert_eq!(\n                market.settle.unwrap(),\n                crypto_pair::normalize_currency(market.settle_id.unwrap().as_str(), $exchange)\n            );\n            // assert!(market.margin);\n            if market.market_type == MarketType::InverseFuture\n                || market.market_type == MarketType::LinearFuture\n                || market.market_type == MarketType::QuantoFuture\n                || market.market_type == MarketType::EuropeanOption\n            {\n                assert!(market.delivery_date.is_some());\n            } else {\n                assert!(market.delivery_date.is_none());\n            }\n        }\n    }};\n}\n"
  },
  {
    "path": "crypto-markets/tests/zb.rs",
    "content": "use crypto_market_type::{get_market_types, MarketType};\nuse crypto_markets::{fetch_markets, fetch_symbols};\nuse crypto_pair::get_market_type;\nuse test_case::test_case;\n\n#[macro_use]\nmod utils;\n\nconst EXCHANGE_NAME: &str = \"zb\";\n\n#[test]\nfn fetch_all_symbols() {\n    gen_all_symbols!();\n}\n\n#[test]\nfn fetch_spot_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::Spot).unwrap();\n    assert!(!symbols.is_empty());\n    for symbol in symbols.iter() {\n        assert!(symbol.contains('_'));\n        assert!(\n            symbol.ends_with(\"_usdt\")\n                || symbol.ends_with(\"_usdc\")\n                || symbol.ends_with(\"_qc\")\n                || symbol.ends_with(\"_btc\")\n        );\n        assert_eq!(symbol.to_string(), symbol.to_lowercase());\n        assert_eq!(MarketType::Spot, get_market_type(symbol, EXCHANGE_NAME, None));\n    }\n}\n\n#[test]\nfn fetch_linear_swap_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::LinearSwap).unwrap();\n    assert!(!symbols.is_empty());\n    for symbol in symbols.iter() {\n        assert!(symbol.ends_with(\"_USDT\"));\n        // assert!(symbol.ends_with(\"_USDT\") || symbol.ends_with(\"_QC\"));\n        assert_eq!(MarketType::LinearSwap, get_market_type(symbol, EXCHANGE_NAME, None));\n    }\n}\n\n#[test]\nfn fetch_spot_markets() {\n    let markets = fetch_markets(EXCHANGE_NAME, MarketType::Spot).unwrap();\n    assert!(!markets.is_empty());\n\n    let btc_usdt = markets.iter().find(|m| m.symbol == \"btc_usdt\").unwrap().clone();\n    assert_eq!(btc_usdt.market_type, MarketType::Spot);\n    assert!(btc_usdt.contract_value.is_none());\n    assert_eq!(btc_usdt.precision.tick_size, 0.01);\n    assert_eq!(btc_usdt.precision.lot_size, 0.0001);\n    let quantity_limit = btc_usdt.quantity_limit.unwrap();\n    assert_eq!(quantity_limit.min.unwrap(), 0.0001);\n    assert!(quantity_limit.max.is_none());\n}\n\n#[test]\nfn fetch_linear_swap_markets() {\n    let markets = fetch_markets(EXCHANGE_NAME, MarketType::LinearSwap).unwrap();\n    assert!(!markets.is_empty());\n\n    let btc_usdt = markets.iter().find(|m| m.symbol == \"BTC_USDT\").unwrap().clone();\n    assert_eq!(btc_usdt.market_type, MarketType::LinearSwap);\n    assert_eq!(btc_usdt.contract_value, Some(1.0));\n    assert_eq!(btc_usdt.precision.tick_size, 0.01);\n    assert_eq!(btc_usdt.precision.lot_size, 0.001);\n    let quantity_limit = btc_usdt.quantity_limit.unwrap();\n    assert_eq!(quantity_limit.min.unwrap(), 0.001);\n    assert_eq!(quantity_limit.max, Some(1000.0));\n}\n\n#[test_case(MarketType::LinearSwap)]\nfn test_contract_values(market_type: MarketType) {\n    check_contract_values!(EXCHANGE_NAME, market_type);\n}\n"
  },
  {
    "path": "crypto-markets/tests/zbg.rs",
    "content": "use crypto_market_type::{get_market_types, MarketType};\nuse crypto_markets::{fetch_markets, fetch_symbols};\nuse crypto_pair::get_market_type;\nuse test_case::test_case;\n\n#[macro_use]\nmod utils;\n\nconst EXCHANGE_NAME: &str = \"zbg\";\n\n#[test]\nfn fetch_all_symbols() {\n    gen_all_symbols!();\n}\n\n#[test]\nfn fetch_spot_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::Spot).unwrap();\n    assert!(!symbols.is_empty());\n    for symbol in symbols.iter() {\n        assert!(symbol.contains('_'));\n        assert_eq!(symbol.to_string(), symbol.to_lowercase());\n        assert_eq!(MarketType::Spot, get_market_type(symbol, EXCHANGE_NAME, None));\n    }\n}\n\n#[test]\nfn fetch_inverse_swap_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::InverseSwap).unwrap();\n    assert!(!symbols.is_empty());\n    for symbol in symbols.iter() {\n        assert!(symbol.ends_with(\"_USD-R\"));\n        assert_eq!(MarketType::InverseSwap, get_market_type(symbol, EXCHANGE_NAME, None));\n    }\n}\n\n#[test]\nfn fetch_linear_swap_symbols() {\n    let symbols = fetch_symbols(EXCHANGE_NAME, MarketType::LinearSwap).unwrap();\n    assert!(!symbols.is_empty());\n    for symbol in symbols.iter() {\n        assert!(symbol.ends_with(\"_USDT\") || symbol.ends_with(\"_ZUSD\"));\n        assert_eq!(MarketType::LinearSwap, get_market_type(symbol, EXCHANGE_NAME, None));\n    }\n}\n\n#[test]\nfn fetch_spot_markets() {\n    let markets = fetch_markets(EXCHANGE_NAME, MarketType::Spot).unwrap();\n    assert!(!markets.is_empty());\n\n    let btc_usdt = markets.iter().find(|m| m.symbol == \"btc_usdt\").unwrap().clone();\n    assert_eq!(btc_usdt.market_type, MarketType::Spot);\n    assert!(btc_usdt.contract_value.is_none());\n    assert_eq!(btc_usdt.precision.tick_size, 0.1);\n    assert_eq!(btc_usdt.precision.lot_size, 0.000001);\n    let quantity_limit = btc_usdt.quantity_limit.unwrap();\n    assert_eq!(quantity_limit.min.unwrap(), 0.000001);\n    assert!(quantity_limit.max.is_none());\n}\n\n#[test]\nfn fetch_inverse_swap_markets() {\n    let markets = fetch_markets(EXCHANGE_NAME, MarketType::InverseSwap).unwrap();\n    assert!(!markets.is_empty());\n\n    let btcusd_perp = markets.iter().find(|m| m.symbol == \"BTC_USD-R\").unwrap().clone();\n    assert_eq!(btcusd_perp.market_type, MarketType::InverseSwap);\n    assert_eq!(btcusd_perp.contract_value, Some(1.0));\n    assert_eq!(btcusd_perp.precision.tick_size, 0.5);\n    assert_eq!(btcusd_perp.precision.lot_size, 1.0);\n    assert!(btcusd_perp.quantity_limit.is_none());\n}\n\n#[test]\nfn fetch_linear_swap_markets() {\n    let markets = fetch_markets(EXCHANGE_NAME, MarketType::LinearSwap).unwrap();\n    assert!(!markets.is_empty());\n\n    let btcusdt = markets.iter().find(|m| m.symbol == \"BTC_USDT\").unwrap().clone();\n    assert_eq!(btcusdt.market_type, MarketType::LinearSwap);\n    assert_eq!(btcusdt.contract_value, Some(0.01));\n    assert_eq!(btcusdt.precision.tick_size, 0.5);\n    assert_eq!(btcusdt.precision.lot_size, 1.0);\n    assert!(btcusdt.quantity_limit.is_none());\n}\n\n#[test_case(MarketType::InverseSwap)]\n#[test_case(MarketType::LinearSwap)]\nfn test_contract_values(market_type: MarketType) {\n    check_contract_values!(EXCHANGE_NAME, market_type);\n}\n"
  },
  {
    "path": "crypto-msg-type/Cargo.toml",
    "content": "[package]\nname = \"crypto-msg-type\"\nversion = \"1.0.11\"\nauthors = [\"soulmachine <soulmachine@gmail.com>\"]\nedition = \"2021\"\ndescription = \"Cryptocurrenty message type\"\nlicense = \"Apache-2.0\"\nrepository = \"https://github.com/crypto-crawler/crypto-crawler-rs/tree/main/crypto-msg-type\"\nkeywords = [\"cryptocurrency\", \"blockchain\", \"trading\"]\n\n[dependencies]\nserde = { version = \"1\", features = [\"derive\"] }\nserde_json = \"1\"\nstrum = \"0.24\"\nstrum_macros = \"0.24\"\n"
  },
  {
    "path": "crypto-msg-type/include/crypto_msg_type.h",
    "content": "/* Licensed under Apache-2.0 */\n#ifndef CRYPTO_MSG_TYPE_H_\n#define CRYPTO_MSG_TYPE_H_\n\n/**\n * Crypto message types.\n *\n * L2Snapshot and L2TopK are very similar, the former is from RESTful API,\n * the latter is from websocket.\n */\ntypedef enum {\n  /**\n   * All other messages\n   */\n  Other,\n  /**\n   * tick-by-tick trade messages\n   */\n  Trade,\n  /**\n   * Incremental level2 orderbook updates\n   */\n  L2Event,\n  /**\n   * Level2 snapshot from RESTful API\n   */\n  L2Snapshot,\n  /**\n   * Level2 top K snapshots from websocket\n   */\n  L2TopK,\n  /**\n   * Incremental level3 orderbook updates\n   */\n  L3Event,\n  /**\n   * Level3 snapshot from RESTful API\n   */\n  L3Snapshot,\n  /**\n   * Best bid and ask\n   */\n  BBO,\n  /**\n   * 24hr rolling window ticker\n   */\n  Ticker,\n  /**\n   * OHLCV candlestick\n   */\n  Candlestick,\n  /**\n   * Funding rate\n   */\n  FundingRate,\n  /**\n   * Open interest\n   */\n  OpenInterest,\n  /**\n   * Long/short ratio\n   */\n  LongShortRatio,\n  /**\n   * Taker buy/sell volume\n   */\n  TakerVolume,\n} MessageType;\n\n#endif /* CRYPTO_MSG_TYPE_H_ */\n"
  },
  {
    "path": "crypto-msg-type/src/exchanges/binance.rs",
    "content": "use std::collections::HashMap;\n\nuse crate::MessageType;\n\nfn msg_type_to_channel(msg_type: MessageType) -> &'static str {\n    match msg_type {\n        MessageType::Trade => \"aggTrade\",\n        MessageType::L2Event => \"depth@100ms\",\n        MessageType::L2TopK => \"depth5\",\n        MessageType::BBO => \"bookTicker\",\n        MessageType::Ticker => \"ticker\",\n        MessageType::Candlestick => \"kline\",\n        _ => panic!(\"Unknown message type {msg_type}\"),\n    }\n}\n\nfn channel_symbol_to_topic(\n    channel: &str,\n    symbol: &str,\n    configs: Option<&HashMap<String, String>>,\n) -> String {\n    if channel == \"kline\" {\n        format!(\"{}@kline_{}\", symbol.to_lowercase(), configs.unwrap().get(\"interval\").unwrap())\n    } else {\n        format!(\"{}@{}\", symbol.to_lowercase(), channel)\n    }\n}\n\nfn topics_to_command(topics: &[String], subscribe: bool) -> String {\n    // spot requires `id`, otherwise it returns the error:\n    // {\"error\":{\"code\":2,\"msg\":\"Invalid request: request ID must be an unsigned\n    // integer\"}}\n    format!(\n        r#\"{{\"id\":9527, \"method\":\"{}\",\"params\":{}}}\"#,\n        if subscribe { \"SUBSCRIBE\" } else { \"UNSUBSCRIBE\" },\n        serde_json::to_string(topics).unwrap()\n    )\n}\n\npub(crate) fn get_ws_commands(\n    msg_types: &[MessageType],\n    symbols: &[String],\n    subscribe: bool,\n    configs: Option<&HashMap<String, String>>,\n) -> Vec<String> {\n    let topics = msg_types\n        .iter()\n        .map(|msg_type| msg_type_to_channel(*msg_type))\n        .flat_map(|channel| {\n            symbols.iter().map(|symbol| channel_symbol_to_topic(channel, symbol, configs))\n        })\n        .collect::<Vec<String>>();\n    vec![topics_to_command(&topics, subscribe)]\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn single_msg_type_multiple_symbols() {\n        let commands = get_ws_commands(\n            &[MessageType::Trade],\n            &[\"BTCUSDT\".to_string(), \"ETHUSDT\".to_string()],\n            true,\n            None,\n        );\n        assert_eq!(commands.len(), 1);\n        assert_eq!(\n            r#\"{\"id\":9527, \"method\":\"SUBSCRIBE\",\"params\":[\"btcusdt@aggTrade\",\"ethusdt@aggTrade\"]}\"#,\n            commands[0]\n        );\n    }\n\n    #[test]\n    fn multiple_msg_types_single_symbol() {\n        let commands = get_ws_commands(\n            &[MessageType::Trade, MessageType::L2Event],\n            &[\"BTCUSDT\".to_string()],\n            true,\n            None,\n        );\n        assert_eq!(commands.len(), 1);\n        assert_eq!(\n            r#\"{\"id\":9527, \"method\":\"SUBSCRIBE\",\"params\":[\"btcusdt@aggTrade\",\"btcusdt@depth@100ms\"]}\"#,\n            commands[0]\n        );\n    }\n\n    #[test]\n    fn candlestick() {\n        let mut configs = HashMap::new();\n        configs.insert(\"interval\".to_string(), \"1m\".to_string());\n        let commands = get_ws_commands(\n            &[MessageType::Candlestick],\n            &[\"BTCUSDT\".to_string(), \"ETHUSDT\".to_string()],\n            true,\n            Some(&configs),\n        );\n        assert_eq!(commands.len(), 1);\n        assert_eq!(\n            r#\"{\"id\":9527, \"method\":\"SUBSCRIBE\",\"params\":[\"btcusdt@kline_1m\",\"ethusdt@kline_1m\"]}\"#,\n            commands[0]\n        );\n    }\n}\n"
  },
  {
    "path": "crypto-msg-type/src/exchanges/bitfinex.rs",
    "content": "use std::collections::HashMap;\n\nuse crate::MessageType;\n\nfn msg_type_symbol_to_command(\n    msg_type: MessageType,\n    symbol: &str,\n    subscribe: bool,\n    configs: Option<&HashMap<String, String>>,\n) -> String {\n    let sub_or_unsub = if subscribe { \"subscribe\" } else { \"unsubscribe\" };\n    match msg_type {\n        MessageType::Trade | MessageType::Ticker => {\n            let channel = if msg_type == MessageType::Trade { \"trades\" } else { \"ticker\" };\n            format!(r#\"{{\"event\":\"{sub_or_unsub}\", \"channel\":\"{channel}\", \"symbol\":\"{symbol}\"}}\"#)\n        }\n        MessageType::L2Event => format!(\n            r#\"{{\"event\":\"{sub_or_unsub}\", \"channel\":\"book\", \"symbol\":\"{symbol}\", \"prec\":\"P0\", \"frec\":\"F0\", \"len\":25}}\"#\n        ),\n        MessageType::L3Event | MessageType::BBO => format!(\n            r#\"{{\"event\":\"{}\", \"channel\":\"book\", \"symbol\": \"{}\", \"prec\":\"R0\", \"len\": {}}}\"#,\n            sub_or_unsub,\n            symbol,\n            if msg_type == MessageType::L3Event { 25 } else { 1 }\n        ),\n        MessageType::Candlestick => format!(\n            r#\"{{\"event\":\"{}\", \"channel\":\"candles\", \"key\":\"trade:{}:{}\"}}\"#,\n            sub_or_unsub,\n            configs.unwrap().get(\"interval\").unwrap(),\n            symbol\n        ),\n        _ => panic!(\"Unknown message type {msg_type}\"),\n    }\n}\n\npub(crate) fn get_ws_commands(\n    msg_types: &[MessageType],\n    symbols: &[String],\n    subscribe: bool,\n    configs: Option<&HashMap<String, String>>,\n) -> Vec<String> {\n    msg_types\n        .iter()\n        .flat_map(|msg_type| {\n            symbols\n                .iter()\n                .map(|symbol| msg_type_symbol_to_command(*msg_type, symbol, subscribe, configs))\n        })\n        .collect::<Vec<String>>()\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn single_msg_type_multiple_symbols() {\n        let commands = get_ws_commands(\n            &[MessageType::Trade],\n            &[\"tBTCUST\".to_string(), \"tETHUST\".to_string()],\n            true,\n            None,\n        );\n        assert_eq!(commands.len(), 2);\n        assert_eq!(r#\"{\"event\":\"subscribe\", \"channel\":\"trades\", \"symbol\":\"tBTCUST\"}\"#, commands[0]);\n        assert_eq!(r#\"{\"event\":\"subscribe\", \"channel\":\"trades\", \"symbol\":\"tETHUST\"}\"#, commands[1]);\n    }\n\n    #[test]\n    fn multiple_msg_types_single_symbol() {\n        let commands = get_ws_commands(\n            &[MessageType::Trade, MessageType::L2Event],\n            &[\"tBTCUST\".to_string()],\n            true,\n            None,\n        );\n        assert_eq!(commands.len(), 2);\n        assert_eq!(r#\"{\"event\":\"subscribe\", \"channel\":\"trades\", \"symbol\":\"tBTCUST\"}\"#, commands[0]);\n        assert_eq!(\n            r#\"{\"event\":\"subscribe\", \"channel\":\"book\", \"symbol\":\"tBTCUST\", \"prec\":\"P0\", \"frec\":\"F0\", \"len\":25}\"#,\n            commands[1]\n        );\n    }\n\n    #[test]\n    fn candlestick() {\n        let mut configs = HashMap::new();\n        configs.insert(\"interval\".to_string(), \"1m\".to_string());\n        let commands = get_ws_commands(\n            &[MessageType::Candlestick],\n            &[\"tBTCUST\".to_string(), \"tETHUST\".to_string()],\n            true,\n            Some(&configs),\n        );\n        assert_eq!(commands.len(), 2);\n        assert_eq!(\n            r#\"{\"event\":\"subscribe\", \"channel\":\"candles\", \"key\":\"trade:1m:tBTCUST\"}\"#,\n            commands[0]\n        );\n        assert_eq!(\n            r#\"{\"event\":\"subscribe\", \"channel\":\"candles\", \"key\":\"trade:1m:tETHUST\"}\"#,\n            commands[1]\n        );\n    }\n}\n"
  },
  {
    "path": "crypto-msg-type/src/exchanges/bitmex.rs",
    "content": "use std::collections::HashMap;\n\nuse crate::MessageType;\n\nfn msg_type_to_channel(msg_type: MessageType) -> &'static str {\n    match msg_type {\n        MessageType::Trade => \"trade\",\n        MessageType::L2Event => \"orderBookL2_25\",\n        MessageType::L2TopK => \"orderBook10\",\n        MessageType::BBO => \"quote\",\n        MessageType::Candlestick => \"tradeBin\",\n        _ => panic!(\"Unknown message type {msg_type}\"),\n    }\n}\n\nfn channel_symbol_to_topic(\n    channel: &str,\n    symbol: &str,\n    configs: Option<&HashMap<String, String>>,\n) -> String {\n    if channel == \"tradeBin\" {\n        format!(\"tradeBin{}:{}\", configs.unwrap().get(\"interval\").unwrap(), symbol)\n    } else {\n        format!(\"{channel}:{symbol}\")\n    }\n}\n\nfn topics_to_command(topics: &[String], subscribe: bool) -> String {\n    format!(\n        r#\"{{\"op\":\"{}\", \"args\":{}}}\"#,\n        if subscribe { \"subscribe\" } else { \"unsubscribe\" },\n        serde_json::to_string(&topics).unwrap()\n    )\n}\n\npub(crate) fn get_ws_commands(\n    msg_types: &[MessageType],\n    symbols: &[String],\n    subscribe: bool,\n    configs: Option<&HashMap<String, String>>,\n) -> Vec<String> {\n    let topics = msg_types\n        .iter()\n        .map(|msg_type| msg_type_to_channel(*msg_type))\n        .flat_map(|channel| {\n            symbols.iter().map(|symbol| channel_symbol_to_topic(channel, symbol, configs))\n        })\n        .collect::<Vec<String>>();\n    vec![topics_to_command(&topics, subscribe)]\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn single_msg_type_multiple_symbols() {\n        let commands = get_ws_commands(\n            &[MessageType::Trade],\n            &[\"XBTUSD\".to_string(), \"ETHUSD\".to_string()],\n            true,\n            None,\n        );\n        assert_eq!(commands.len(), 1);\n        assert_eq!(r#\"{\"op\":\"subscribe\", \"args\":[\"trade:XBTUSD\",\"trade:ETHUSD\"]}\"#, commands[0]);\n    }\n\n    #[test]\n    fn multiple_msg_types_single_symbol() {\n        let commands = get_ws_commands(\n            &[MessageType::Trade, MessageType::L2Event],\n            &[\"XBTUSD\".to_string()],\n            true,\n            None,\n        );\n        assert_eq!(commands.len(), 1);\n        assert_eq!(\n            r#\"{\"op\":\"subscribe\", \"args\":[\"trade:XBTUSD\",\"orderBookL2_25:XBTUSD\"]}\"#,\n            commands[0]\n        );\n    }\n\n    #[test]\n    fn candlestick() {\n        let mut configs = HashMap::new();\n        configs.insert(\"interval\".to_string(), \"1m\".to_string());\n        let commands = get_ws_commands(\n            &[MessageType::Candlestick],\n            &[\"XBTUSD\".to_string(), \"ETHUSD\".to_string()],\n            true,\n            Some(&configs),\n        );\n        assert_eq!(commands.len(), 1);\n        assert_eq!(\n            r#\"{\"op\":\"subscribe\", \"args\":[\"tradeBin1m:XBTUSD\",\"tradeBin1m:ETHUSD\"]}\"#,\n            commands[0]\n        );\n    }\n}\n"
  },
  {
    "path": "crypto-msg-type/src/exchanges/bybit.rs",
    "content": "use std::collections::HashMap;\n\nuse crate::MessageType;\n\nfn msg_type_to_channel(msg_type: MessageType) -> &'static str {\n    match msg_type {\n        MessageType::Trade => \"trade\",\n        MessageType::L2Event => \"orderBookL2_25\",\n        MessageType::Ticker => \"instrument_info.100ms\",\n        MessageType::Candlestick => \"klineV2\",\n        _ => panic!(\"Unknown message type {msg_type}\"),\n    }\n}\n\nfn channel_symbol_to_topic(\n    channel: &str,\n    symbol: &str,\n    configs: Option<&HashMap<String, String>>,\n) -> String {\n    if channel == \"klineV2\" {\n        let interval_str = configs.unwrap().get(\"interval\").unwrap();\n        if symbol.ends_with(\"USDT\") {\n            format!(\"candle.{interval_str}.{symbol}\")\n        } else {\n            format!(\"klineV2.{interval_str}.{symbol}\")\n        }\n    } else {\n        format!(\"{channel}.{symbol}\")\n    }\n}\n\nfn topics_to_command(topics: &[String], subscribe: bool) -> String {\n    format!(\n        r#\"{{\"op\":\"{}\", \"args\":{}}}\"#,\n        if subscribe { \"subscribe\" } else { \"unsubscribe\" },\n        serde_json::to_string(&topics).unwrap()\n    )\n}\n\npub(crate) fn get_ws_commands(\n    msg_types: &[MessageType],\n    symbols: &[String],\n    subscribe: bool,\n    configs: Option<&HashMap<String, String>>,\n) -> Vec<String> {\n    let topics = msg_types\n        .iter()\n        .map(|msg_type| msg_type_to_channel(*msg_type))\n        .flat_map(|channel| {\n            symbols.iter().map(|symbol| channel_symbol_to_topic(channel, symbol, configs))\n        })\n        .collect::<Vec<String>>();\n    vec![topics_to_command(&topics, subscribe)]\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn single_msg_type_multiple_symbols() {\n        let commands = get_ws_commands(\n            &[MessageType::Trade],\n            &[\"BTCUSD\".to_string(), \"ETHUSD\".to_string()],\n            true,\n            None,\n        );\n        assert_eq!(commands.len(), 1);\n        assert_eq!(r#\"{\"op\":\"subscribe\", \"args\":[\"trade.BTCUSD\",\"trade.ETHUSD\"]}\"#, commands[0]);\n    }\n\n    #[test]\n    fn multiple_msg_types_single_symbol() {\n        let commands = get_ws_commands(\n            &[MessageType::Trade, MessageType::L2Event],\n            &[\"BTCUSD\".to_string()],\n            true,\n            None,\n        );\n        assert_eq!(commands.len(), 1);\n        assert_eq!(\n            r#\"{\"op\":\"subscribe\", \"args\":[\"trade.BTCUSD\",\"orderBookL2_25.BTCUSD\"]}\"#,\n            commands[0]\n        );\n    }\n\n    #[test]\n    fn candlestick() {\n        let mut configs = HashMap::new();\n        configs.insert(\"interval\".to_string(), \"1\".to_string());\n        let commands = get_ws_commands(\n            &[MessageType::Candlestick],\n            &[\"BTCUSD\".to_string(), \"ETHUSD\".to_string()],\n            true,\n            Some(&configs),\n        );\n        assert_eq!(commands.len(), 1);\n        assert_eq!(\n            r#\"{\"op\":\"subscribe\", \"args\":[\"klineV2.1.BTCUSD\",\"klineV2.1.ETHUSD\"]}\"#,\n            commands[0]\n        );\n    }\n}\n"
  },
  {
    "path": "crypto-msg-type/src/exchanges/deribit.rs",
    "content": "use std::collections::HashMap;\n\nuse crate::MessageType;\n\nfn msg_type_symbol_to_topic(\n    msg_type: MessageType,\n    symbol: &str,\n    configs: Option<&HashMap<String, String>>,\n) -> String {\n    match msg_type {\n        MessageType::Trade => format!(\"trades.{symbol}.100ms\"),\n        MessageType::L2Event => format!(\"book.{symbol}.100ms\"),\n        MessageType::L2TopK => format!(\"book.{symbol}.5.10.100ms\"),\n        MessageType::BBO => format!(\"quote.{symbol}\"),\n        MessageType::Candlestick => {\n            format!(\"chart.trades.{}.{}\", symbol, configs.unwrap().get(\"interval\").unwrap())\n        }\n        MessageType::Ticker => format!(\"ticker.{symbol}.100ms\"),\n        _ => panic!(\"Unknown message type {msg_type}\"),\n    }\n}\n\nfn topics_to_command(topics: &[String], subscribe: bool) -> String {\n    format!(\n        r#\"{{\"method\":\"public/{}\", \"params\":{{\"channels\":{}}}}}\"#,\n        if subscribe { \"subscribe\" } else { \"unsubscribe\" },\n        serde_json::to_string(topics).unwrap()\n    )\n}\n\npub(crate) fn get_ws_commands(\n    msg_types: &[MessageType],\n    symbols: &[String],\n    subscribe: bool,\n    configs: Option<&HashMap<String, String>>,\n) -> Vec<String> {\n    let topics = msg_types\n        .iter()\n        .flat_map(|msg_type| {\n            symbols.iter().map(|symbol| msg_type_symbol_to_topic(*msg_type, symbol, configs))\n        })\n        .collect::<Vec<String>>();\n    vec![topics_to_command(&topics, subscribe)]\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn single_msg_type_multiple_symbols() {\n        let commands = get_ws_commands(\n            &[MessageType::Trade],\n            &[\"BTC-PERPETUAL\".to_string(), \"ETH-PERPETUAL\".to_string()],\n            true,\n            None,\n        );\n        assert_eq!(commands.len(), 1);\n        assert_eq!(\n            r#\"{\"method\":\"public/subscribe\", \"params\":{\"channels\":[\"trades.BTC-PERPETUAL.100ms\",\"trades.ETH-PERPETUAL.100ms\"]}}\"#,\n            commands[0]\n        );\n    }\n\n    #[test]\n    fn multiple_msg_types_single_symbol() {\n        let commands = get_ws_commands(\n            &[MessageType::Trade, MessageType::L2Event],\n            &[\"BTC-PERPETUAL\".to_string()],\n            true,\n            None,\n        );\n        assert_eq!(commands.len(), 1);\n        assert_eq!(\n            r#\"{\"method\":\"public/subscribe\", \"params\":{\"channels\":[\"trades.BTC-PERPETUAL.100ms\",\"book.BTC-PERPETUAL.100ms\"]}}\"#,\n            commands[0]\n        );\n    }\n\n    #[test]\n    fn candlestick() {\n        let mut configs = HashMap::new();\n        configs.insert(\"interval\".to_string(), \"1m\".to_string());\n        let commands = get_ws_commands(\n            &[MessageType::Candlestick],\n            &[\"BTC-PERPETUAL\".to_string(), \"ETH-PERPETUAL\".to_string()],\n            true,\n            Some(&configs),\n        );\n        assert_eq!(commands.len(), 1);\n        assert_eq!(\n            r#\"{\"method\":\"public/subscribe\", \"params\":{\"channels\":[\"chart.trades.BTC-PERPETUAL.1m\",\"chart.trades.ETH-PERPETUAL.1m\"]}}\"#,\n            commands[0]\n        );\n    }\n}\n"
  },
  {
    "path": "crypto-msg-type/src/exchanges/ftx.rs",
    "content": "use std::collections::HashMap;\n\nuse crate::MessageType;\n\nfn msg_type_to_channel(msg_type: MessageType) -> &'static str {\n    match msg_type {\n        MessageType::Trade => \"trades\",\n        MessageType::L2Event => \"orderbook\",\n        MessageType::BBO => \"ticker\",\n        _ => panic!(\"Unknown message type {msg_type}\"),\n    }\n}\n\nfn channel_symbol_to_command(channel: &str, symbol: &str, subscribe: bool) -> String {\n    format!(\n        r#\"{{\"op\":\"{}\",\"channel\":\"{}\",\"market\":\"{}\"}}\"#,\n        if subscribe { \"subscribe\" } else { \"unsubscribe\" },\n        channel,\n        symbol,\n    )\n}\n\npub(crate) fn get_ws_commands(\n    msg_types: &[MessageType],\n    symbols: &[String],\n    subscribe: bool,\n    _configs: Option<&HashMap<String, String>>,\n) -> Vec<String> {\n    msg_types\n        .iter()\n        .map(|msg_type| msg_type_to_channel(*msg_type))\n        .flat_map(|channel| {\n            symbols.iter().map(|symbol| channel_symbol_to_command(channel, symbol, subscribe))\n        })\n        .collect::<Vec<String>>()\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn single_msg_type_multiple_symbols() {\n        let commands = get_ws_commands(\n            &[MessageType::Trade],\n            &[\"BTC/USD\".to_string(), \"BTC-PERP\".to_string()],\n            true,\n            None,\n        );\n        assert_eq!(commands.len(), 2);\n        println!(\"{}\", commands[0]);\n        assert_eq!(r#\"{\"op\":\"subscribe\",\"channel\":\"trades\",\"market\":\"BTC/USD\"}\"#, commands[0]);\n        assert_eq!(r#\"{\"op\":\"subscribe\",\"channel\":\"trades\",\"market\":\"BTC-PERP\"}\"#, commands[1]);\n    }\n\n    #[test]\n    fn multiple_msg_types_single_symbol() {\n        let commands = get_ws_commands(\n            &[MessageType::Trade, MessageType::L2Event],\n            &[\"BTC-PERP\".to_string()],\n            true,\n            None,\n        );\n        assert_eq!(commands.len(), 2);\n        assert_eq!(r#\"{\"op\":\"subscribe\",\"channel\":\"trades\",\"market\":\"BTC-PERP\"}\"#, commands[0]);\n        assert_eq!(r#\"{\"op\":\"subscribe\",\"channel\":\"orderbook\",\"market\":\"BTC-PERP\"}\"#, commands[1]);\n    }\n}\n"
  },
  {
    "path": "crypto-msg-type/src/exchanges/huobi.rs",
    "content": "use std::collections::HashMap;\n\nuse crate::MessageType;\n\nfn msg_type_symbol_to_topic(\n    msg_type: MessageType,\n    symbol: &str,\n    configs: Option<&HashMap<String, String>>,\n) -> String {\n    let is_spot = symbol.to_lowercase() == *symbol;\n    let channel = match msg_type {\n        MessageType::Trade => \"trade.detail\",\n        MessageType::L2Event => {\n            if is_spot {\n                \"mbp.20\"\n            } else {\n                \"depth.size_20.high_freq\"\n            }\n        }\n        MessageType::L2TopK => {\n            if is_spot {\n                \"depth.step1\"\n            } else {\n                \"depth.step7\"\n            }\n        }\n        MessageType::BBO => \"bbo\",\n        MessageType::Ticker => \"detail\",\n        MessageType::Candlestick => \"kline\",\n        _ => panic!(\"Unknown message type {msg_type}\"),\n    };\n    if msg_type == MessageType::Candlestick {\n        format!(\"market.{}.kline.{}\", symbol, configs.unwrap().get(\"interval\").unwrap())\n    } else {\n        format!(\"market.{symbol}.{channel}\")\n    }\n}\n\nfn topic_to_command(topic: &str, subscribe: bool) -> String {\n    if topic.ends_with(\"depth.size_20.high_freq\") {\n        format!(\n            r#\"{{\"{}\": \"{}\",\"data_type\":\"incremental\",\"id\": \"crypto-ws-client\"}}\"#,\n            if subscribe { \"sub\" } else { \"unsub\" },\n            topic,\n        )\n    } else {\n        format!(\n            r#\"{{\"{}\":\"{}\",\"id\":\"crypto-ws-client\"}}\"#,\n            if subscribe { \"sub\" } else { \"unsub\" },\n            topic,\n        )\n    }\n}\n\npub(crate) fn get_ws_commands(\n    msg_types: &[MessageType],\n    symbols: &[String],\n    subscribe: bool,\n    configs: Option<&HashMap<String, String>>,\n) -> Vec<String> {\n    msg_types\n        .iter()\n        .flat_map(|msg_type| {\n            symbols.iter().map(|symbol| msg_type_symbol_to_topic(*msg_type, symbol, configs))\n        })\n        .map(|topic| topic_to_command(&topic, subscribe))\n        .collect()\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn single_msg_type_multiple_symbols() {\n        let commands = get_ws_commands(\n            &[MessageType::Trade],\n            &[\"BTC-USD\".to_string(), \"ETH-USD\".to_string()],\n            true,\n            None,\n        );\n        assert_eq!(commands.len(), 2);\n        assert_eq!(r#\"{\"sub\":\"market.BTC-USD.trade.detail\",\"id\":\"crypto-ws-client\"}\"#, commands[0]);\n        assert_eq!(r#\"{\"sub\":\"market.ETH-USD.trade.detail\",\"id\":\"crypto-ws-client\"}\"#, commands[1]);\n    }\n\n    #[test]\n    fn multiple_msg_types_single_symbol() {\n        let commands = get_ws_commands(\n            &[MessageType::Trade, MessageType::L2Event],\n            &[\"BTC-USD\".to_string()],\n            true,\n            None,\n        );\n        assert_eq!(commands.len(), 2);\n        assert_eq!(r#\"{\"sub\":\"market.BTC-USD.trade.detail\",\"id\":\"crypto-ws-client\"}\"#, commands[0]);\n        assert_eq!(\n            r#\"{\"sub\": \"market.BTC-USD.depth.size_20.high_freq\",\"data_type\":\"incremental\",\"id\": \"crypto-ws-client\"}\"#,\n            commands[1]\n        );\n    }\n\n    #[test]\n    fn candlestick() {\n        let mut configs = HashMap::new();\n        configs.insert(\"interval\".to_string(), \"1m\".to_string());\n        let commands = get_ws_commands(\n            &[MessageType::Candlestick],\n            &[\"BTC-USD\".to_string(), \"ETH-USD\".to_string()],\n            true,\n            Some(&configs),\n        );\n        assert_eq!(commands.len(), 2);\n        assert_eq!(r#\"{\"sub\":\"market.BTC-USD.kline.1m\",\"id\":\"crypto-ws-client\"}\"#, commands[0]);\n        assert_eq!(r#\"{\"sub\":\"market.ETH-USD.kline.1m\",\"id\":\"crypto-ws-client\"}\"#, commands[1]);\n    }\n}\n"
  },
  {
    "path": "crypto-msg-type/src/exchanges/mod.rs",
    "content": "pub(super) mod binance;\npub(super) mod bitfinex;\npub(super) mod bitmex;\npub(super) mod bybit;\npub(super) mod deribit;\npub(super) mod ftx;\npub(super) mod huobi;\npub(super) mod okex;\npub(super) mod okx;\n"
  },
  {
    "path": "crypto-msg-type/src/exchanges/okex.rs",
    "content": "use std::collections::HashMap;\n\nuse crate::MessageType;\n\nfn get_market_type(symbol: &str) -> &'static str {\n    if symbol.ends_with(\"-SWAP\") {\n        \"swap\"\n    } else {\n        let c = symbol.matches('-').count();\n        if c == 1 {\n            \"spot\"\n        } else if c == 2 {\n            let date = &symbol[(symbol.len() - 6)..];\n            debug_assert!(date.parse::<i64>().is_ok());\n            \"futures\"\n        } else {\n            debug_assert!(symbol.ends_with(\"-C\") || symbol.ends_with(\"-P\"));\n            \"option\"\n        }\n    }\n}\n\nfn msg_type_to_channel(msg_type: MessageType) -> &'static str {\n    match msg_type {\n        MessageType::Trade => \"trade\",\n        MessageType::L2Event => \"depth_l2_tbt\",\n        MessageType::L2TopK => \"depth5\",\n        MessageType::BBO => \"ticker\",\n        MessageType::Ticker => \"ticker\",\n        MessageType::Candlestick => \"candle\",\n        _ => panic!(\"Unknown message type {msg_type}\"),\n    }\n}\n\nfn channel_symbol_to_topic(\n    channel: &str,\n    symbol: &str,\n    configs: Option<&HashMap<String, String>>,\n) -> String {\n    let market_type = get_market_type(symbol);\n    if channel == \"candle\" {\n        format!(\"{}/candle{}s:{}\", market_type, configs.unwrap().get(\"interval\").unwrap(), symbol)\n    } else {\n        format!(\"{market_type}/{channel}:{symbol}\")\n    }\n}\n\nfn topics_to_command(topics: &[String], subscribe: bool) -> String {\n    format!(\n        r#\"{{\"op\":\"{}\",\"args\":{}}}\"#,\n        if subscribe { \"subscribe\" } else { \"unsubscribe\" },\n        serde_json::to_string(topics).unwrap()\n    )\n}\n\npub(crate) fn get_ws_commands(\n    msg_types: &[MessageType],\n    symbols: &[String],\n    subscribe: bool,\n    configs: Option<&HashMap<String, String>>,\n) -> Vec<String> {\n    let topics = msg_types\n        .iter()\n        .map(|msg_type| msg_type_to_channel(*msg_type))\n        .flat_map(|channel| {\n            symbols.iter().map(|symbol| channel_symbol_to_topic(channel, symbol, configs))\n        })\n        .collect::<Vec<String>>();\n    vec![topics_to_command(&topics, subscribe)]\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn single_msg_type_multiple_symbols() {\n        let commands = get_ws_commands(\n            &[MessageType::Trade],\n            &[\"BTC-USDT-SWAP\".to_string(), \"ETH-USDT-SWAP\".to_string()],\n            true,\n            None,\n        );\n        assert_eq!(commands.len(), 1);\n        assert_eq!(\n            r#\"{\"op\":\"subscribe\",\"args\":[\"swap/trade:BTC-USDT-SWAP\",\"swap/trade:ETH-USDT-SWAP\"]}\"#,\n            commands[0]\n        );\n    }\n\n    #[test]\n    fn multiple_msg_types_single_symbol() {\n        let commands = get_ws_commands(\n            &[MessageType::Trade, MessageType::L2Event],\n            &[\"BTC-USDT-SWAP\".to_string()],\n            true,\n            None,\n        );\n        assert_eq!(commands.len(), 1);\n        assert_eq!(\n            r#\"{\"op\":\"subscribe\",\"args\":[\"swap/trade:BTC-USDT-SWAP\",\"swap/depth_l2_tbt:BTC-USDT-SWAP\"]}\"#,\n            commands[0]\n        );\n    }\n\n    #[test]\n    fn candlestick() {\n        let mut configs = HashMap::new();\n        configs.insert(\"interval\".to_string(), \"60\".to_string());\n        let commands = get_ws_commands(\n            &[MessageType::Candlestick],\n            &[\"BTC-USDT-SWAP\".to_string(), \"ETH-USDT-SWAP\".to_string()],\n            true,\n            Some(&configs),\n        );\n        assert_eq!(commands.len(), 1);\n        assert_eq!(\n            r#\"{\"op\":\"subscribe\",\"args\":[\"swap/candle60s:BTC-USDT-SWAP\",\"swap/candle60s:ETH-USDT-SWAP\"]}\"#,\n            commands[0]\n        );\n    }\n}\n"
  },
  {
    "path": "crypto-msg-type/src/exchanges/okx.rs",
    "content": "use std::collections::{BTreeMap, HashMap};\n\nuse crate::MessageType;\n\nfn msg_type_to_channel(msg_type: MessageType) -> &'static str {\n    match msg_type {\n        MessageType::Trade => \"trades\",\n        MessageType::L2Event => \"books-l2-tbt\",\n        MessageType::L2TopK => \"books5\",\n        MessageType::Ticker => \"tickers\",\n        MessageType::Candlestick => \"candle\",\n        _ => panic!(\"Unknown message type {msg_type}\"),\n    }\n}\n\nfn channel_symbol_to_topic(\n    channel: &str,\n    symbol: &str,\n    configs: Option<&HashMap<String, String>>,\n) -> String {\n    if channel == \"candle\" {\n        format!(\"candle{}:{}\", configs.unwrap().get(\"interval\").unwrap(), symbol)\n    } else {\n        format!(\"{channel}:{symbol}\")\n    }\n}\n\nfn topics_to_command(topics: &[String], subscribe: bool) -> String {\n    let arr = topics\n        .iter()\n        .map(|s| {\n            let mut map = BTreeMap::new();\n            let v: Vec<&str> = s.split(':').collect();\n            let channel = v[0];\n            let symbol = v[1];\n            map.insert(\"channel\".to_string(), channel.to_string());\n            map.insert(\"instId\".to_string(), symbol.to_string());\n            map\n        })\n        .collect::<Vec<BTreeMap<String, String>>>();\n    format!(\n        r#\"{{\"op\":\"{}\",\"args\":{}}}\"#,\n        if subscribe { \"subscribe\" } else { \"unsubscribe\" },\n        serde_json::to_string(&arr).unwrap(),\n    )\n}\n\npub(crate) fn get_ws_commands(\n    msg_types: &[MessageType],\n    symbols: &[String],\n    subscribe: bool,\n    configs: Option<&HashMap<String, String>>,\n) -> Vec<String> {\n    let topics = msg_types\n        .iter()\n        .map(|msg_type| msg_type_to_channel(*msg_type))\n        .flat_map(|channel| {\n            symbols.iter().map(|symbol| channel_symbol_to_topic(channel, symbol, configs))\n        })\n        .collect::<Vec<String>>();\n    vec![topics_to_command(&topics, subscribe)]\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn single_msg_type_multiple_symbols() {\n        let commands = get_ws_commands(\n            &[MessageType::Trade],\n            &[\"BTC-USDT-SWAP\".to_string(), \"ETH-USDT-SWAP\".to_string()],\n            true,\n            None,\n        );\n        assert_eq!(commands.len(), 1);\n        assert_eq!(\n            r#\"{\"op\":\"subscribe\",\"args\":[{\"channel\":\"trades\",\"instId\":\"BTC-USDT-SWAP\"},{\"channel\":\"trades\",\"instId\":\"ETH-USDT-SWAP\"}]}\"#,\n            commands[0]\n        );\n    }\n\n    #[test]\n    fn multiple_msg_types_single_symbol() {\n        let commands = get_ws_commands(\n            &[MessageType::Trade, MessageType::L2Event],\n            &[\"BTC-USDT-SWAP\".to_string()],\n            true,\n            None,\n        );\n        assert_eq!(commands.len(), 1);\n        assert_eq!(\n            r#\"{\"op\":\"subscribe\",\"args\":[{\"channel\":\"trades\",\"instId\":\"BTC-USDT-SWAP\"},{\"channel\":\"books-l2-tbt\",\"instId\":\"BTC-USDT-SWAP\"}]}\"#,\n            commands[0]\n        );\n    }\n\n    #[test]\n    fn candlestick() {\n        let mut configs = HashMap::new();\n        configs.insert(\"interval\".to_string(), \"1m\".to_string());\n        let commands = get_ws_commands(\n            &[MessageType::Candlestick],\n            &[\"BTC-USDT-SWAP\".to_string(), \"ETH-USDT-SWAP\".to_string()],\n            true,\n            Some(&configs),\n        );\n        assert_eq!(commands.len(), 1);\n        assert_eq!(\n            r#\"{\"op\":\"subscribe\",\"args\":[{\"channel\":\"candle1m\",\"instId\":\"BTC-USDT-SWAP\"},{\"channel\":\"candle1m\",\"instId\":\"ETH-USDT-SWAP\"}]}\"#,\n            commands[0]\n        );\n    }\n}\n"
  },
  {
    "path": "crypto-msg-type/src/lib.rs",
    "content": "mod exchanges;\n\nuse std::collections::HashMap;\n\nuse serde::{Deserialize, Serialize};\nuse strum_macros::{Display, EnumString};\n\n/// Crypto message types.\n///\n/// L2Snapshot and L2TopK are very similar, the former is from RESTful API,\n/// the latter is from websocket.\n#[repr(C)]\n#[derive(Copy, Clone, Eq, PartialEq, Serialize, Deserialize, Display, Debug, EnumString, Hash)]\n#[serde(rename_all = \"snake_case\")]\n#[strum(serialize_all = \"snake_case\")]\npub enum MessageType {\n    /// All other messages\n    Other,\n    /// tick-by-tick trade messages\n    Trade,\n    /// Incremental level2 orderbook updates\n    L2Event,\n    /// Level2 snapshot from RESTful API\n    L2Snapshot,\n    /// Level2 top K snapshots from websocket\n    #[serde(rename = \"l2_topk\")]\n    #[strum(serialize = \"l2_topk\")]\n    L2TopK,\n    /// Incremental level3 orderbook updates\n    L3Event,\n    /// Level3 snapshot from RESTful API\n    L3Snapshot,\n    /// Best bid and ask\n    #[serde(rename = \"bbo\")]\n    #[allow(clippy::upper_case_acronyms)]\n    BBO,\n    /// 24hr rolling window ticker\n    Ticker,\n    /// OHLCV candlestick\n    Candlestick,\n    /// Funding rate\n    FundingRate,\n    /// Open interest\n    OpenInterest,\n    /// Long/short ratio\n    LongShortRatio,\n    /// Taker buy/sell volume\n    TakerVolume,\n}\n\n/// Translate to websocket subscribe/unsubscribe commands.\n///\n/// `configs` Some `msg_type` requires a config, for example,\n/// `Candlestick` requires an `interval` parameter.\npub fn get_ws_commands(\n    exchange: &str,\n    msg_types: &[MessageType],\n    symbols: &[String],\n    subscribe: bool,\n    configs: Option<&HashMap<String, String>>,\n) -> Vec<String> {\n    if msg_types.is_empty() || symbols.is_empty() {\n        return Vec::new();\n    }\n    match exchange {\n        \"binance\" => exchanges::binance::get_ws_commands(msg_types, symbols, subscribe, configs),\n        \"bitfinex\" => exchanges::bitfinex::get_ws_commands(msg_types, symbols, subscribe, configs),\n        \"bitmex\" => exchanges::bitmex::get_ws_commands(msg_types, symbols, subscribe, configs),\n        \"bybit\" => exchanges::bybit::get_ws_commands(msg_types, symbols, subscribe, configs),\n        \"deribit\" => exchanges::deribit::get_ws_commands(msg_types, symbols, subscribe, configs),\n        \"ftx\" => exchanges::ftx::get_ws_commands(msg_types, symbols, subscribe, configs),\n        \"huobi\" => exchanges::huobi::get_ws_commands(msg_types, symbols, subscribe, configs),\n        \"okex\" => exchanges::okex::get_ws_commands(msg_types, symbols, subscribe, configs),\n        \"okx\" => exchanges::okx::get_ws_commands(msg_types, symbols, subscribe, configs),\n        _ => Vec::new(),\n    }\n}\n"
  },
  {
    "path": "crypto-rest-client/Cargo.toml",
    "content": "[package]\nname = \"crypto-rest-client\"\nversion = \"1.0.1\"\nauthors = [\"soulmachine <soulmachine@gmail.com>\"]\nedition = \"2021\"\ndescription   = \"An RESTful client for all cryptocurrency exchanges.\"\nlicense = \"Apache-2.0\"\nrepository = \"https://github.com/crypto-crawler/crypto-crawler-rs/tree/main/crypto-rest-client\"\nkeywords = [\"cryptocurrency\", \"blockchain\", \"trading\"]\n\n[dependencies]\ncrypto-market-type = \"1.1.5\"\nonce_cell = \"1.17.1\"\nlog = \"0.4.17\"\nregex = \"1.7.1\"\nreqwest = { version = \"0.11.14\", features = [\"blocking\", \"gzip\", \"socks\"] }\nserde = { version = \"1.0.152\", features = [\"derive\"] }\nserde_json = \"1.0.93\"\n\n[dev_dependencies]\ntest-case = \"1\"\n"
  },
  {
    "path": "crypto-rest-client/README.md",
    "content": "# crypto-rest-client\n\nAn RESTful client for all cryptocurrency exchanges.\n\n## Example\n\n```rust\nuse crypto_rest_client::{BinanceClient};\n\nfn main() {\n    let config: HashMap<&str, &str> = vec![\n        (\"api_key\", \"your-API-key\"),\n        (\"api_secret\", \"your-API-secret\"),\n    ].into_iter().collect();\n\n    let rest_client = BinanceClient::new(config);\n\n    // buy\n    let transaction_id = rest_client.place_order(\"Spot\", \"btcusdt\", 27999.9, 5.0, false);\n    println!(\"{}\", transactionId);\n}\n```\n\n## Supported Exchanges\n\n-   Binance\n-   Huobi\n-   OKEx\n"
  },
  {
    "path": "crypto-rest-client/src/error.rs",
    "content": "use std::{error::Error as StdError, fmt};\n\npub(crate) type Result<T> = std::result::Result<T, Error>;\n\n#[derive(Debug)]\npub struct Error(pub String);\n\nimpl fmt::Display for Error {\n    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {\n        write!(f, \"{}\", self.0)\n    }\n}\n\nimpl StdError for Error {}\n\nimpl From<reqwest::Error> for Error {\n    fn from(err: reqwest::Error) -> Self {\n        Error(err.to_string())\n    }\n}\n\nimpl From<serde_json::Error> for Error {\n    fn from(err: serde_json::Error) -> Self {\n        Error(err.to_string())\n    }\n}\n"
  },
  {
    "path": "crypto-rest-client/src/exchanges/binance/binance_inverse.rs",
    "content": "use super::{super::utils::http_get, utils::*};\nuse crate::error::Result;\nuse std::collections::BTreeMap;\n\nconst BASE_URL: &str = \"https://dapi.binance.com\";\n\n/// Binance Coin-margined Future and Swap market\n///\n/// * REST API doc: <https://binance-docs.github.io/apidocs/delivery/en/>\n/// * Trading at: <https://www.binance.com/en/delivery/btcusd_perpetual>\n/// Rate Limits: <https://binance-docs.github.io/apidocs/delivery/en/#limits>\n///   * 2400 request weight per minute\npub struct BinanceInverseRestClient {\n    _api_key: Option<String>,\n    _api_secret: Option<String>,\n}\n\nimpl BinanceInverseRestClient {\n    pub fn new(api_key: Option<String>, api_secret: Option<String>) -> Self {\n        BinanceInverseRestClient { _api_key: api_key, _api_secret: api_secret }\n    }\n\n    /// Get compressed, aggregate trades.\n    ///\n    /// Equivalent to `/dapi/v1/aggTrades` with `limit=1000`\n    ///\n    /// For example:\n    ///\n    /// - <https://dapi.binance.com/dapi/v1/aggTrades?symbol=BTCUSD_PERP&limit=1000>\n    /// - <https://dapi.binance.com/dapi/v1/aggTrades?symbol=BTCUSD_210625&limit=1000>\n    pub fn fetch_agg_trades(\n        symbol: &str,\n        from_id: Option<u64>,\n        start_time: Option<u64>,\n        end_time: Option<u64>,\n    ) -> Result<String> {\n        check_symbol(symbol);\n        let symbol = Some(symbol);\n        let limit = Some(1000);\n        gen_api_binance!(\"/dapi/v1/aggTrades\", symbol, from_id, start_time, end_time, limit)\n    }\n\n    /// Get a Level2 snapshot of orderbook.\n    ///\n    /// Equivalent to `/dapi/v1/depth` with `limit=1000`\n    ///\n    /// For example:\n    ///\n    /// - <https://dapi.binance.com/dapi/v1/depth?symbol=BTCUSD_PERP&limit=1000>\n    /// - <https://dapi.binance.com/dapi/v1/depth?symbol=BTCUSD_211231&limit=1000>\n    pub fn fetch_l2_snapshot(symbol: &str) -> Result<String> {\n        check_symbol(symbol);\n        let symbol = Some(symbol);\n        let limit = Some(1000);\n        gen_api_binance!(\"/dapi/v1/depth\", symbol, limit)\n    }\n\n    /// Get open interest.\n    ///\n    /// For example:\n    ///\n    /// - <https://dapi.binance.com/dapi/v1/openInterest?symbol=BTCUSD_PERP>\n    /// - <https://dapi.binance.com/dapi/v1/openInterest?symbol=BTCUSD_211231>\n    pub fn fetch_open_interest(symbol: &str) -> Result<String> {\n        check_symbol(symbol);\n        let symbol = Some(symbol);\n        gen_api_binance!(\"/dapi/v1/openInterest\", symbol)\n    }\n}\n"
  },
  {
    "path": "crypto-rest-client/src/exchanges/binance/binance_linear.rs",
    "content": "use super::{super::utils::http_get, utils::*};\nuse crate::error::Result;\nuse std::collections::BTreeMap;\n\nconst BASE_URL: &str = \"https://fapi.binance.com\";\n\n/// Binance USDT-margined Future and Swap market.\n///\n/// * REST API doc: <https://binance-docs.github.io/apidocs/futures/en/>\n/// * Trading at: <https://www.binance.com/en/futures/BTC_USDT>\n/// * Rate Limits: <https://binance-docs.github.io/apidocs/futures/en/#limits>\n///   * 2400 request weight per minute\npub struct BinanceLinearRestClient {\n    _api_key: Option<String>,\n    _api_secret: Option<String>,\n}\n\nimpl BinanceLinearRestClient {\n    pub fn new(api_key: Option<String>, api_secret: Option<String>) -> Self {\n        BinanceLinearRestClient { _api_key: api_key, _api_secret: api_secret }\n    }\n\n    /// Get compressed, aggregate trades.\n    ///\n    /// Equivalent to `/fapi/v1/aggTrades` with `limit=1000`\n    ///\n    /// For example:\n    ///\n    /// - <https://fapi.binance.com/fapi/v1/aggTrades?symbol=BTCUSDT&limit=1000>\n    /// - <https://fapi.binance.com/fapi/v1/aggTrades?symbol=BTCUSDT_210625&limit=1000>\n    pub fn fetch_agg_trades(\n        symbol: &str,\n        from_id: Option<u64>,\n        start_time: Option<u64>,\n        end_time: Option<u64>,\n    ) -> Result<String> {\n        check_symbol(symbol);\n        let symbol = Some(symbol);\n        let limit = Some(1000);\n        gen_api_binance!(\"/fapi/v1/aggTrades\", symbol, from_id, start_time, end_time, limit)\n    }\n\n    /// Get a Level2 snapshot of orderbook.\n    ///\n    /// Equivalent to `/fapi/v1/depth` with `limit=1000`\n    ///\n    /// For example:\n    ///\n    /// - <https://fapi.binance.com/fapi/v1/depth?symbol=BTCUSDT&limit=1000>\n    /// - <https://fapi.binance.com/fapi/v1/depth?symbol=BTCUSDT_211231&limit=1000>\n    pub fn fetch_l2_snapshot(symbol: &str) -> Result<String> {\n        check_symbol(symbol);\n        let symbol = Some(symbol);\n        let limit = Some(1000);\n        gen_api_binance!(\"/fapi/v1/depth\", symbol, limit)\n    }\n\n    /// Get open interest.\n    ///\n    /// For example:\n    ///\n    /// - <https://fapi.binance.com/fapi/v1/openInterest?symbol=BTCUSDT>\n    /// - <https://fapi.binance.com/fapi/v1/openInterest?symbol=BTCUSDT_211231>\n    pub fn fetch_open_interest(symbol: &str) -> Result<String> {\n        check_symbol(symbol);\n        let symbol = Some(symbol);\n        gen_api_binance!(\"/fapi/v1/openInterest\", symbol)\n    }\n}\n"
  },
  {
    "path": "crypto-rest-client/src/exchanges/binance/binance_option.rs",
    "content": "use super::{super::utils::http_get, utils::*};\nuse crate::error::Result;\nuse std::collections::BTreeMap;\n\nconst BASE_URL: &str = \"https://vapi.binance.com\";\n\n/// Binance Option market.\n///\n/// * REST API doc: <https://binance-docs.github.io/apidocs/voptions/en/>\n/// * Trading at: <https://voptions.binance.com/en>\npub struct BinanceOptionRestClient {\n    _api_key: Option<String>,\n    _api_secret: Option<String>,\n}\n\nimpl BinanceOptionRestClient {\n    pub fn new(api_key: Option<String>, api_secret: Option<String>) -> Self {\n        BinanceOptionRestClient { _api_key: api_key, _api_secret: api_secret }\n    }\n\n    /// Get most recent trades.\n    ///\n    /// 500 recent trades are returned.\n    ///\n    /// For example: <https://voptions.binance.com/options-api/v1/public/market/trades?symbol=BTC-210129-40000-C&limit=500&t=1609956688000>\n    pub fn fetch_trades(symbol: &str, start_time: Option<u64>) -> Result<String> {\n        check_symbol(symbol);\n        let t = start_time;\n        gen_api_binance!(format!(\"/vapi/v1/trades?symbol={symbol}&limit=500\"), t)\n    }\n\n    /// Get a Level2 snapshot of orderbook.\n    ///\n    /// For example: <https://vapi.binance.com/vapi/v1/depth?symbol=BTC-211001-30000-P&limit=1000>\n    pub fn fetch_l2_snapshot(symbol: &str) -> Result<String> {\n        check_symbol(symbol);\n        let symbol = Some(symbol);\n        let limit = Some(1000);\n        gen_api_binance!(\"/vapi/v1/depth\", symbol, limit)\n    }\n}\n"
  },
  {
    "path": "crypto-rest-client/src/exchanges/binance/binance_spot.rs",
    "content": "use super::{super::utils::http_get, utils::*};\nuse crate::error::Result;\nuse std::collections::BTreeMap;\n\nconst BASE_URL: &str = \"https://api.binance.com\";\n\n/// Binance Spot market.\n///\n/// * RESTful API doc: <https://binance-docs.github.io/apidocs/spot/en/>\n/// * Trading at: <https://www.binance.com/en/trade/BTC_USDT>\n/// * Rate Limits: <https://binance-docs.github.io/apidocs/spot/en/#limits>\n///   * 1200 request weight per minute\n///   * 6100 raw requests per 5 minutes\npub struct BinanceSpotRestClient {\n    _api_key: Option<String>,\n    _api_secret: Option<String>,\n}\n\nimpl BinanceSpotRestClient {\n    pub fn new(api_key: Option<String>, api_secret: Option<String>) -> Self {\n        BinanceSpotRestClient { _api_key: api_key, _api_secret: api_secret }\n    }\n\n    /// Get compressed, aggregate trades.\n    ///\n    /// Equivalent to `/api/v3/aggTrades` with `limit=1000`\n    ///\n    /// For example: <https://api.binance.com/api/v3/aggTrades?symbol=BTCUSDT&limit=1000>\n    pub fn fetch_agg_trades(\n        symbol: &str,\n        from_id: Option<u64>,\n        start_time: Option<u64>,\n        end_time: Option<u64>,\n    ) -> Result<String> {\n        check_symbol(symbol);\n        let symbol = Some(symbol);\n        let limit = Some(1000);\n        gen_api_binance!(\"/api/v3/aggTrades\", symbol, from_id, start_time, end_time, limit)\n    }\n\n    /// Get a Level2 snapshot of orderbook.\n    ///\n    /// Equivalent to `/api/v3/depth` with `limit=1000`\n    ///\n    /// For example: <https://api.binance.com/api/v3/depth?symbol=BTCUSDT&limit=1000>\n    pub fn fetch_l2_snapshot(symbol: &str) -> Result<String> {\n        check_symbol(symbol);\n        let symbol = Some(symbol);\n        let limit = Some(1000);\n        gen_api_binance!(\"/api/v3/depth\", symbol, limit)\n    }\n}\n"
  },
  {
    "path": "crypto-rest-client/src/exchanges/binance/mod.rs",
    "content": "#[macro_use]\nmod utils;\n\npub(crate) mod binance_inverse;\npub(crate) mod binance_linear;\npub(crate) mod binance_option;\npub(crate) mod binance_spot;\n\nuse crate::error::Result;\nuse crypto_market_type::MarketType;\n\npub(crate) fn fetch_l2_snapshot(market_type: MarketType, symbol: &str) -> Result<String> {\n    let func = match market_type {\n        MarketType::Spot => binance_spot::BinanceSpotRestClient::fetch_l2_snapshot,\n        MarketType::InverseFuture | MarketType::InverseSwap => {\n            binance_inverse::BinanceInverseRestClient::fetch_l2_snapshot\n        }\n        MarketType::LinearFuture | MarketType::LinearSwap => {\n            binance_linear::BinanceLinearRestClient::fetch_l2_snapshot\n        }\n        MarketType::EuropeanOption => binance_option::BinanceOptionRestClient::fetch_l2_snapshot,\n        _ => panic!(\"Binance unknown market_type: {market_type}\"),\n    };\n\n    func(symbol)\n}\n\npub(crate) fn fetch_open_interest(market_type: MarketType, symbol: &str) -> Result<String> {\n    let func = match market_type {\n        MarketType::InverseFuture | MarketType::InverseSwap => {\n            binance_inverse::BinanceInverseRestClient::fetch_open_interest\n        }\n        MarketType::LinearFuture | MarketType::LinearSwap => {\n            binance_linear::BinanceLinearRestClient::fetch_open_interest\n        }\n        _ => panic!(\"Binance {market_type} does not have open interest data\"),\n    };\n    func(symbol)\n}\n"
  },
  {
    "path": "crypto-rest-client/src/exchanges/binance/utils.rs",
    "content": "use std::collections::BTreeMap;\n\nuse crate::error::{Error, Result};\n\nuse once_cell::sync::Lazy;\nuse regex::Regex;\nuse serde_json::Value;\n\nstatic SYMBOL_PATTERN: Lazy<Regex> = Lazy::new(|| Regex::new(\"^[A-Z0-9-_.]{1,20}$\").unwrap());\n\npub(super) fn check_symbol(symbol: &str) {\n    if !SYMBOL_PATTERN.is_match(symbol) {\n        panic!(\"Illegal symbol {symbol}, legal symbol should be '^[A-Z0-9-_.]{{1,20}}$'.\");\n    }\n}\n\npub(super) fn check_code_in_body(resp: String) -> Result<String> {\n    let obj = serde_json::from_str::<BTreeMap<String, Value>>(&resp);\n    if obj.is_err() {\n        return Ok(resp);\n    }\n\n    match obj.unwrap().get(\"code\") {\n        Some(code) => {\n            if code.as_i64().unwrap() != 0 {\n                Err(Error(resp))\n            } else {\n                Ok(resp)\n            }\n        }\n        None => Ok(resp),\n    }\n}\n\nmacro_rules! gen_api_binance {\n    ( $path:expr$(, $param_name:ident )* ) => {\n        {\n            #[allow(unused_mut)]\n            let mut params = BTreeMap::new();\n            $(\n                if let Some(param_name) = $param_name {\n                    params.insert(stringify!($param_name).to_string(), param_name.to_string());\n                }\n            )*\n            let ret = http_get(format!(\"{}{}\",BASE_URL, $path).as_str(), &params);\n            match ret {\n                Ok(resp) => check_code_in_body(resp),\n                Err(_) => ret,\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "crypto-rest-client/src/exchanges/bitfinex.rs",
    "content": "use super::utils::http_get;\nuse crate::error::Result;\nuse std::collections::BTreeMap;\n\nconst BASE_URL: &str = \"https://api-pub.bitfinex.com\";\n\n/// The REST client for Bitfinex, including all markets.\n///\n/// * REST API doc: <https://docs.bitfinex.com/docs/rest-general>\n/// * Spot: <https://trading.bitfinex.com/trading>\n/// * Swap: <https://trading.bitfinex.com/t/BTCF0:USTF0>\n/// * Funding: <https://trading.bitfinex.com/funding>\npub struct BitfinexRestClient {\n    _api_key: Option<String>,\n    _api_secret: Option<String>,\n}\n\nimpl BitfinexRestClient {\n    pub fn new(api_key: Option<String>, api_secret: Option<String>) -> Self {\n        BitfinexRestClient { _api_key: api_key, _api_secret: api_secret }\n    }\n\n    /// /v2/trades/Symbol/hist\n    pub fn fetch_trades(\n        symbol: &str,\n        limit: Option<u16>,\n        start: Option<u64>,\n        end: Option<u64>,\n        sort: Option<i8>,\n    ) -> Result<String> {\n        gen_api!(format!(\"/v2/trades/{symbol}/hist\"), limit, start, end, sort)\n    }\n\n    /// Get a Level2 snapshot of orderbook.\n    ///\n    /// Equivalent to `/v2/book/Symbol/P0` with `len=100`\n    ///\n    /// For example: <https://api-pub.bitfinex.com/v2/book/tBTCUSD/P0?len=100>\n    ///\n    /// Ratelimit: 90 req/min\n    pub fn fetch_l2_snapshot(symbol: &str) -> Result<String> {\n        let len = Some(100);\n        gen_api!(format!(\"/v2/book/{symbol}/P0\"), len)\n    }\n\n    /// Get a Level3 snapshot of orderbook.\n    ///\n    /// Equivalent to `/v2/book/Symbol/R0` with `len=100`\n    ///\n    /// For example: <https://api-pub.bitfinex.com/v2/book/tBTCUSD/R0?len=100>\n    pub fn fetch_l3_snapshot(symbol: &str) -> Result<String> {\n        let len = Some(100);\n        gen_api!(format!(\"/v2/book/{symbol}/R0\"), len)\n    }\n}\n"
  },
  {
    "path": "crypto-rest-client/src/exchanges/bitget/bitget_spot.rs",
    "content": "use super::super::utils::http_get;\nuse crate::error::Result;\nuse std::collections::BTreeMap;\n\nconst BASE_URL: &str = \"https://api.bitget.com\";\n\n/// The RESTful client for Bitget spot market.\n///\n/// * RESTful API doc: <https://bitgetlimited.github.io/apidoc/en/spot/>\n/// * Trading at: <https://www.bitget.com/spot/>\npub struct BitgetSpotRestClient {\n    _api_key: Option<String>,\n    _api_secret: Option<String>,\n}\n\nimpl BitgetSpotRestClient {\n    pub fn new(api_key: Option<String>, api_secret: Option<String>) -> Self {\n        BitgetSpotRestClient { _api_key: api_key, _api_secret: api_secret }\n    }\n\n    /// Get the latest Level2 snapshot of orderbook.\n    ///\n    /// Top 150 bids and asks are returned.\n    ///\n    /// For example: <https://api.bitget.com/api/spot/v1/market/depth?symbol=BTCUSDT_SPBL&type=step0>,\n    pub fn fetch_l2_snapshot(symbol: &str) -> Result<String> {\n        gen_api!(format!(\"/api/spot/v1/market/depth?symbol={symbol}&type=step0\"))\n    }\n}\n"
  },
  {
    "path": "crypto-rest-client/src/exchanges/bitget/bitget_swap.rs",
    "content": "use super::super::utils::http_get;\nuse crate::error::Result;\nuse std::collections::BTreeMap;\n\nconst BASE_URL: &str = \"https://api.bitget.com\";\n\n/// The RESTful client for Bitget swap markets.\n///\n/// * RESTful API doc: <https://bitgetlimited.github.io/apidoc/en/mix/#restapi>\n/// * Trading at: <https://www.bitget.com/mix/>\npub struct BitgetSwapRestClient {\n    _api_key: Option<String>,\n    _api_secret: Option<String>,\n}\n\nimpl BitgetSwapRestClient {\n    pub fn new(api_key: Option<String>, api_secret: Option<String>) -> Self {\n        BitgetSwapRestClient { _api_key: api_key, _api_secret: api_secret }\n    }\n\n    /// Get the latest Level2 snapshot of orderbook.\n    ///\n    /// For example: <https://api.bitget.com/api/mix/v1/market/depth?symbol=BTCUSDT_UMCBL&limit=100>\n    ///\n    /// Rate Limit：20 requests per 2 seconds\n    pub fn fetch_l2_snapshot(symbol: &str) -> Result<String> {\n        gen_api!(format!(\"/api/mix/v1/market/depth?symbol={symbol}&limit=100\"))\n    }\n\n    /// Get open interest.\n    ///\n    /// For example:\n    ///\n    /// - <https://api.bitget.com/api/mix/v1/market/open-interest?symbol=BTCUSDT_UMCBL>\n    pub fn fetch_open_interest(symbol: &str) -> Result<String> {\n        gen_api!(format!(\"/api/mix/v1/market/open-interest?symbol={symbol}\"))\n    }\n}\n"
  },
  {
    "path": "crypto-rest-client/src/exchanges/bitget/mod.rs",
    "content": "mod bitget_spot;\nmod bitget_swap;\n\npub use bitget_spot::BitgetSpotRestClient;\npub use bitget_swap::BitgetSwapRestClient;\n\nuse crate::error::Result;\nuse crypto_market_type::MarketType;\n\npub(crate) fn fetch_l2_snapshot(market_type: MarketType, symbol: &str) -> Result<String> {\n    let func = match market_type {\n        MarketType::Spot => bitget_spot::BitgetSpotRestClient::fetch_l2_snapshot,\n        MarketType::InverseFuture | MarketType::InverseSwap | MarketType::LinearSwap => {\n            bitget_swap::BitgetSwapRestClient::fetch_l2_snapshot\n        }\n        _ => panic!(\"Bitget unknown market_type: {market_type}\"),\n    };\n\n    func(symbol)\n}\n\npub(crate) fn fetch_open_interest(market_type: MarketType, symbol: &str) -> Result<String> {\n    let func = match market_type {\n        MarketType::InverseFuture | MarketType::InverseSwap | MarketType::LinearSwap => {\n            bitget_swap::BitgetSwapRestClient::fetch_open_interest\n        }\n        _ => panic!(\"Bitget {market_type} does not have open interest\"),\n    };\n\n    func(symbol)\n}\n"
  },
  {
    "path": "crypto-rest-client/src/exchanges/bithumb.rs",
    "content": "use super::utils::http_get;\nuse crate::error::Result;\nuse std::collections::BTreeMap;\n\nconst BASE_URL: &str = \"https://global-openapi.bithumb.pro/openapi/v1\";\n\n/// The REST client for Bithumb.\n///\n/// Bithumb has only Spot market.\n///\n/// * REST API doc: <https://github.com/bithumb-pro/bithumb.pro-official-api-docs/blob/master/rest-api.md>\n/// * Trading at: <https://en.bithumb.com/trade/order/BTC_KRW>\n/// * Rate Limits: <https://apidocs.bithumb.com/docs/rate_limits>\n///   * 135 requests per 1 second for public APIs.\n///   * 15 requests per 1 second for private APIs.\npub struct BithumbRestClient {\n    _api_key: Option<String>,\n    _api_secret: Option<String>,\n}\n\nimpl BithumbRestClient {\n    pub fn new(api_key: Option<String>, api_secret: Option<String>) -> Self {\n        BithumbRestClient { _api_key: api_key, _api_secret: api_secret }\n    }\n\n    /// Get most recent trades.\n    ///\n    /// For example: <https://global-openapi.bithumb.pro/openapi/v1/spot/trades?symbol=BTC-USDT>\n    pub fn fetch_trades(symbol: &str) -> Result<String> {\n        gen_api!(format!(\"/spot/trades?symbol={symbol}\"))\n    }\n\n    /// Get the latest Level2 orderbook snapshot.\n    ///\n    /// For example: <https://global-openapi.bithumb.pro/openapi/v1/spot/orderBook?symbol=BTC-USDT>\n    pub fn fetch_l2_snapshot(symbol: &str) -> Result<String> {\n        gen_api!(format!(\"/spot/orderBook?symbol={symbol}\"))\n    }\n}\n"
  },
  {
    "path": "crypto-rest-client/src/exchanges/bitmex.rs",
    "content": "use super::utils::http_get;\nuse crate::error::Result;\nuse std::collections::BTreeMap;\n\nconst BASE_URL: &str = \"https://www.bitmex.com/api/v1\";\n\n/// The REST client for BitMEX.\n///\n/// BitMEX has Swap and Future markets.\n///\n/// * REST API doc: <https://www.bitmex.com/api/explorer/>\n/// * Trading at: <https://www.bitmex.com/app/trade/>\n/// * Rate Limits: <https://www.bitmex.com/app/restAPI#Limits>\n///   * 60 requests per minute on all routes (reduced to 30 when\n///     unauthenticated)\n///   * 10 requests per second on certain routes (see below)\npub struct BitmexRestClient {\n    _api_key: Option<String>,\n    _api_secret: Option<String>,\n}\n\nimpl BitmexRestClient {\n    pub fn new(api_key: Option<String>, api_secret: Option<String>) -> Self {\n        BitmexRestClient { _api_key: api_key, _api_secret: api_secret }\n    }\n\n    /// Get trades from a beginning time.\n    ///\n    /// Equivalent to `/trade` with `count=1000`\n    ///\n    /// For example: <https://www.bitmex.com/api/v1/trade?symbol=XBTUSD&count=1000&startTime=2018-09-28T12:34:25.706Z>\n    #[allow(non_snake_case)]\n    pub fn fetch_trades(symbol: &str, start_time: Option<String>) -> Result<String> {\n        let symbol = Some(symbol);\n        let startTime = start_time;\n        gen_api!(\"/trade\", symbol, startTime)\n    }\n\n    /// Get a full Level2 snapshot of orderbook.\n    ///\n    /// Equivalent to `/orderBook/L2` with `depth=0`\n    ///\n    /// For example: <https://www.bitmex.com/api/v1/orderBook/L2?symbol=XBTUSD&depth=0>\n    pub fn fetch_l2_snapshot(symbol: &str) -> Result<String> {\n        let symbol = Some(symbol);\n        let depth = Some(0);\n        gen_api!(\"/orderBook/L2\", symbol, depth)\n    }\n}\n"
  },
  {
    "path": "crypto-rest-client/src/exchanges/bitstamp.rs",
    "content": "use super::utils::http_get;\nuse crate::error::Result;\nuse std::collections::BTreeMap;\n\nconst BASE_URL: &str = \"https://www.bitstamp.net/api\";\n\n/// The REST client for Bitstamp.\n///\n/// Bitstamp has only Spot market.\n///\n/// * REST API doc: <https://www.bitstamp.net/api/>\n/// * Trading at: <https://www.bitstamp.net/market/tradeview/>\n/// * Rate Limits: <https://www.bitstamp.net/api/#what-is-api>\n///   * Do not make more than 8000 requests per 10 minutes or we will ban your\n///     IP address.\npub struct BitstampRestClient {\n    _api_key: Option<String>,\n    _api_secret: Option<String>,\n}\n\nimpl BitstampRestClient {\n    pub fn new(api_key: Option<String>, api_secret: Option<String>) -> Self {\n        BitstampRestClient { _api_key: api_key, _api_secret: api_secret }\n    }\n\n    /// Get trades.\n    ///\n    /// `/v2/transactions/{symbol}/`\n    ///\n    /// `time` specifies the time interval from which we want the transactions\n    /// to be returned. Possible values are \"minute\", \"hour\" (default) or \"day\".\n    ///\n    /// For example: <https://www.bitstamp.net/api/v2/transactions/btcusd/?time=hour>\n    pub fn fetch_trades(symbol: &str, time: Option<String>) -> Result<String> {\n        gen_api!(format!(\"/v2/transactions/{symbol}/\"), time)\n    }\n\n    /// Get a full Level2 orderbook snapshot.\n    ///\n    /// /// Equivalent to `/order_book/symbol` with `group=1`\n    ///\n    /// For example: <https://www.bitstamp.net/api/v2/order_book/btcusd/>\n    pub fn fetch_l2_snapshot(symbol: &str) -> Result<String> {\n        gen_api!(format!(\"/v2/order_book/{symbol}\"))\n    }\n\n    /// Get a full Level3 orderbook snapshot.\n    ///\n    /// Equivalent to `/order_book/symbol` with `group=2`\n    ///\n    /// For example: <https://www.bitstamp.net/api/v2/order_book/btcusd/?group=2>\n    pub fn fetch_l3_snapshot(symbol: &str) -> Result<String> {\n        gen_api!(format!(\"/v2/order_book/{symbol}?group=2\"))\n    }\n}\n"
  },
  {
    "path": "crypto-rest-client/src/exchanges/bitz/bitz_spot.rs",
    "content": "use super::super::utils::http_get;\nuse crate::error::Result;\nuse std::collections::BTreeMap;\n\nconst BASE_URL: &str = \"https://apiv2.bitz.com\";\n\n/// The RESTful client for BitZ spot market.\n///\n/// * RESTful API doc: <https://apidocv2.bitz.plus/en/>\n/// * Trading at: <https://www.bitz.plus/exchange>\n/// * Rate Limits: <https://apidocv2.bitz.plus/en/#limit>\n///   * no more than 30 times within 1 sec\npub struct BitzSpotRestClient {\n    _api_key: Option<String>,\n    _api_secret: Option<String>,\n}\n\nimpl BitzSpotRestClient {\n    pub fn new(api_key: Option<String>, api_secret: Option<String>) -> Self {\n        BitzSpotRestClient { _api_key: api_key, _api_secret: api_secret }\n    }\n\n    /// Get the latest Level2 snapshot of orderbook.\n    ///\n    /// For example: <https://apiv2.bitz.com/V2/Market/depth?symbol=btc_usdt>,\n    pub fn fetch_l2_snapshot(symbol: &str) -> Result<String> {\n        gen_api!(format!(\"/V2/Market/depth?symbol={symbol}\"))\n    }\n}\n"
  },
  {
    "path": "crypto-rest-client/src/exchanges/bitz/bitz_swap.rs",
    "content": "use super::super::utils::http_get;\nuse crate::error::{Error, Result};\nuse std::collections::{BTreeMap, HashMap};\n\nuse serde::{Deserialize, Serialize};\nuse serde_json::Value;\n\nconst BASE_URL: &str = \"https://apiv2.bitz.com\";\n\n/// The RESTful client for BitZ swap markets.\n///\n/// * RESTful API doc: <https://apidocv2.bitz.plus/en/>\n/// * Trading at: <https://swap.bitz.plus/en/>\n/// * Rate Limits: <https://apidocv2.bitz.plus/en/#limit>\n///   * no more than 30 times within 1 sec\npub struct BitzSwapRestClient {\n    _api_key: Option<String>,\n    _api_secret: Option<String>,\n}\n\nimpl BitzSwapRestClient {\n    pub fn new(api_key: Option<String>, api_secret: Option<String>) -> Self {\n        BitzSwapRestClient { _api_key: api_key, _api_secret: api_secret }\n    }\n\n    /// Get the latest Level2 snapshot of orderbook.\n    ///\n    /// Top 100 bids and asks are returned.\n    ///\n    /// For example: <https://apiv2.bitz.com/V2/Market/getContractOrderBook?contractId=101&depth=100>,\n    pub fn fetch_l2_snapshot(symbol: &str) -> Result<String> {\n        let symbol_id_map = get_symbol_id_map()?;\n        if !symbol_id_map.contains_key(symbol) {\n            return Err(Error(format!(\"Can NOT find contractId for the pair {symbol}\")));\n        }\n        let contract_id = symbol_id_map.get(symbol).unwrap();\n        gen_api!(format!(\"/V2/Market/getContractOrderBook?contractId={contract_id}&depth=100\"))\n    }\n\n    /// Get open interest.\n    ///\n    /// For example: <https://apiv2.bitz.com/V2/Market/getContractTickers>\n    pub fn fetch_open_interest(symbol: Option<&str>) -> Result<String> {\n        if let Some(symbol) = symbol {\n            let symbol_id_map = get_symbol_id_map()?;\n            if !symbol_id_map.contains_key(symbol) {\n                return Err(Error(format!(\"Can NOT find contractId for the pair {symbol}\")));\n            }\n            let contract_id = symbol_id_map.get(symbol).unwrap();\n            gen_api!(format!(\"/V2/Market/getContractTickers?contractId={contract_id}\"))\n        } else {\n            gen_api!(\"/V2/Market/getContractTickers\")\n        }\n    }\n}\n\n#[derive(Clone, Serialize, Deserialize)]\n#[allow(non_snake_case)]\nstruct SwapMarket {\n    contractId: String, // contract id\n    pair: String,       //contract market\n    status: String,\n    #[serde(flatten)]\n    extra: HashMap<String, Value>,\n}\n\n#[derive(Serialize, Deserialize)]\nstruct Response {\n    status: i64,\n    msg: String,\n    data: Vec<SwapMarket>,\n    time: i64,\n    microtime: String,\n    source: String,\n}\n\nfn get_symbol_id_map() -> Result<HashMap<String, String>> {\n    let params = BTreeMap::new();\n    let txt = http_get(\"https://apiv2.bitz.com/Market/getContractCoin\", &params)?;\n    let resp = serde_json::from_str::<Response>(&txt)?;\n    if resp.status != 200 {\n        return Err(Error(txt));\n    }\n\n    let mut symbol_id_map = HashMap::<String, String>::new();\n    for x in resp.data.iter() {\n        if x.status == \"1\" {\n            symbol_id_map.insert(x.pair.clone(), x.contractId.clone());\n        }\n    }\n    Ok(symbol_id_map)\n}\n"
  },
  {
    "path": "crypto-rest-client/src/exchanges/bitz/mod.rs",
    "content": "mod bitz_spot;\nmod bitz_swap;\n\npub use bitz_spot::BitzSpotRestClient;\npub use bitz_swap::BitzSwapRestClient;\n\nuse crate::error::Result;\nuse crypto_market_type::MarketType;\n\npub(crate) fn fetch_l2_snapshot(market_type: MarketType, symbol: &str) -> Result<String> {\n    let func = match market_type {\n        MarketType::Spot => bitz_spot::BitzSpotRestClient::fetch_l2_snapshot,\n        MarketType::InverseSwap | MarketType::LinearSwap => {\n            bitz_swap::BitzSwapRestClient::fetch_l2_snapshot\n        }\n        _ => panic!(\"BitZ unknown market_type: {market_type}\"),\n    };\n\n    func(symbol)\n}\n\npub(crate) fn fetch_open_interest(market_type: MarketType, symbol: Option<&str>) -> Result<String> {\n    let func = match market_type {\n        MarketType::LinearSwap | MarketType::InverseSwap => {\n            bitz_swap::BitzSwapRestClient::fetch_open_interest\n        }\n        _ => panic!(\"bitz {market_type} does not have open interest\"),\n    };\n\n    func(symbol)\n}\n"
  },
  {
    "path": "crypto-rest-client/src/exchanges/bybit.rs",
    "content": "use super::utils::http_get;\nuse crate::error::Result;\nuse std::collections::BTreeMap;\n\nconst BASE_URL: &str = \"https://api.bybit.com/v2\";\n\n/// The RESTful client for Bybit.\n///\n/// Bybit has InverseSwap and LinearSwap markets.\n///\n/// * RESTful API doc: <https://bybit-exchange.github.io/docs/inverse/#t-marketdata>\n/// * Trading at:\n///     * InverseSwap <https://www.bybit.com/trade/inverse/>\n///     * LinearSwap <https://www.bybit.com/trade/usdt/>\n/// * Rate Limit: <https://bybit-exchange.github.io/docs/inverse/#t-ratelimits>\n///   * GET method:\n///     * 50 requests per second continuously for 2 minutes\n///     * 70 requests per second continuously for 5 seconds\n///   * POST method:\n///     * 20 requests per second continuously for 2 minutes\n///     * 50 requests per second continuously for 5 seconds\npub struct BybitRestClient {\n    _api_key: Option<String>,\n    _api_secret: Option<String>,\n}\n\nimpl BybitRestClient {\n    pub fn new(api_key: Option<String>, api_secret: Option<String>) -> Self {\n        BybitRestClient { _api_key: api_key, _api_secret: api_secret }\n    }\n\n    /// Get the latest Level2 snapshot of orderbook.\n    ///\n    /// Top 50 bids and asks are returned.\n    ///\n    /// For example: <https://api.bybit.com/v2/public/orderBook/L2?symbol=BTCUSD>,\n    pub fn fetch_l2_snapshot(symbol: &str) -> Result<String> {\n        gen_api!(format!(\"/public/orderBook/L2?symbol={symbol}\"))\n    }\n\n    /// Get open interest.\n    ///\n    /// For example:\n    ///\n    /// - <https://api.bybit.com/v2/public/open-interest?symbol=BTCUSD&period=5min&limit=200>\n    /// - <https://api.bybit.com/v2/public/open-interest?symbol=BTCUSDT&period=5min&limit=200>\n    /// - <https://api.bybit.com/v2/public/open-interest?symbol=BTCUSDU22&period=5min&limit=200>\n    pub fn fetch_open_interest(symbol: &str) -> Result<String> {\n        gen_api!(format!(\"/public/open-interest?symbol={symbol}&period=5min&limit=200\"))\n    }\n\n    /// Get long-short ratio.\n    ///\n    /// For example:\n    ///\n    /// - <https://api.bybit.com/v2/public/account-ratio?symbol=BTCUSD&period=5min&limit=500>\n    /// - <https://api.bybit.com/v2/public/account-ratio?symbol=BTCUSDT&period=5min&limit=500>\n    /// - <https://api.bybit.com/v2/public/account-ratio?symbol=BTCUSDU22&period=5min&limit=500>\n    pub fn fetch_long_short_ratio(symbol: &str) -> Result<String> {\n        gen_api!(format!(\"/public/account-ratio?symbol={symbol}&period=5min&limit=200\"))\n    }\n}\n"
  },
  {
    "path": "crypto-rest-client/src/exchanges/coinbase_pro.rs",
    "content": "use super::utils::http_get;\nuse crate::error::Result;\nuse std::collections::BTreeMap;\n\nconst BASE_URL: &str = \"https://api.exchange.coinbase.com\";\n\n/// The REST client for CoinbasePro.\n///\n/// CoinbasePro has only Spot market.\n///\n///   * REST API doc: <https://docs.cloud.coinbase.com/exchange/reference>\n///   * Trading at: <https://pro.coinbase.com/>\n///   * Rate Limits: <https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getaccounts#rate-limits>\n///   * This endpoint has a custom rate limit by profile ID: 25 requests per\n///     second, up to 50 requests per second in bursts\npub struct CoinbaseProRestClient {\n    _api_key: Option<String>,\n    _api_secret: Option<String>,\n}\n\nimpl CoinbaseProRestClient {\n    pub fn new(api_key: Option<String>, api_secret: Option<String>) -> Self {\n        CoinbaseProRestClient { _api_key: api_key, _api_secret: api_secret }\n    }\n\n    /// List the latest trades for a product.\n    ///\n    /// `/products/{symbol}/trades`\n    ///\n    /// For example: <https://api.pro.coinbase.com/products/BTC-USD/trades>\n    pub fn fetch_trades(symbol: &str) -> Result<String> {\n        gen_api!(format!(\"/products/{symbol}/trades\"))\n    }\n\n    /// Get the latest Level2 orderbook snapshot.\n    ///\n    /// Top 50 bids and asks (aggregated) are returned.\n    ///\n    /// For example: <https://api.pro.coinbase.com/products/BTC-USD/book?level=2>\n    pub fn fetch_l2_snapshot(symbol: &str) -> Result<String> {\n        gen_api!(format!(\"/products/{symbol}/book?level=2\"))\n    }\n\n    /// Get the latest Level3 orderbook snapshot.\n    ///\n    /// Full order book (non aggregated) are returned.\n    ///\n    /// For example: <https://api.pro.coinbase.com/products/BTC-USD/book?level=3>\n    pub fn fetch_l3_snapshot(symbol: &str) -> Result<String> {\n        gen_api!(format!(\"/products/{symbol}/book?level=3\"))\n    }\n}\n"
  },
  {
    "path": "crypto-rest-client/src/exchanges/deribit.rs",
    "content": "use super::utils::http_get;\nuse crate::error::Result;\nuse std::collections::BTreeMap;\n\nconst BASE_URL: &str = \"https://www.deribit.com/api/v2\";\n\n/// The RESTful client for Deribit.\n///\n/// Deribit has InverseFuture, InverseSwap and Option markets.\n///\n/// * WebSocket API doc: <https://docs.deribit.com/?shell#market-data>\n/// * Trading at:\n///     * Future <https://www.deribit.com/main#/futures>\n///     * Option <https://www.deribit.com/main#/options>\n/// * Rate Limits: <https://www.deribit.com/pages/information/rate-limits>\n///   * Each sub-account has a rate limit of 20 requests per second\npub struct DeribitRestClient {\n    _api_key: Option<String>,\n    _api_secret: Option<String>,\n}\n\nimpl DeribitRestClient {\n    pub fn new(api_key: Option<String>, api_secret: Option<String>) -> Self {\n        DeribitRestClient { _api_key: api_key, _api_secret: api_secret }\n    }\n\n    /// Get most recent trades.\n    ///\n    /// 100 trades are returned.\n    ///\n    /// For example: <https://www.deribit.com/api/v2/public/get_last_trades_by_instrument?count=100&instrument_name=BTC-PERPETUAL>\n    pub fn fetch_trades(symbol: &str) -> Result<String> {\n        gen_api!(format!(\n            \"/public/get_last_trades_by_instrument?count=100&instrument_name={symbol}\"\n        ))\n    }\n\n    /// Get the latest Level2 snapshot of orderbook.\n    ///\n    /// Top 2000 bids and asks are returned.\n    ///\n    /// For example: <https://www.deribit.com/api/v2/public/get_order_book?depth=2000&instrument_name=BTC-PERPETUAL>,\n    pub fn fetch_l2_snapshot(symbol: &str) -> Result<String> {\n        gen_api!(format!(\"/public/get_order_book?depth=2000&instrument_name={symbol}\",))\n    }\n\n    /// Get open interest.\n    ///\n    /// For example:\n    /// - <https://www.deribit.com/api/v2/public/get_book_summary_by_currency?currency=BTC>\n    /// - <https://www.deribit.com/api/v2/public/get_book_summary_by_instrument?instrument_name=BTC-PERPETUAL>\n    pub fn fetch_open_interest(symbol: Option<&str>) -> Result<String> {\n        if let Some(symbol) = symbol {\n            gen_api!(format!(\"/public/get_book_summary_by_instrument?instrument_name={symbol}\"))\n        } else {\n            let btc = gen_api!(\"/public/get_book_summary_by_currency?currency=BTC\")?;\n            let eth = gen_api!(\"/public/get_book_summary_by_currency?currency=ETH\")?;\n            let sol = gen_api!(\"/public/get_book_summary_by_currency?currency=SOL\")?;\n            let usdc = gen_api!(\"/public/get_book_summary_by_currency?currency=USDC\")?;\n            Ok(format!(\"{btc}\\n{eth}\\n{sol}\\n{usdc}\"))\n        }\n    }\n}\n"
  },
  {
    "path": "crypto-rest-client/src/exchanges/dydx/dydx_swap.rs",
    "content": "use super::super::utils::http_get;\nuse crate::error::Result;\nuse std::collections::BTreeMap;\n\nconst BASE_URL: &str = \"https://api.dydx.exchange\";\n\n/// dYdX perpetual RESTful client.\n///\n/// * REST API doc: <https://docs.dydx.exchange/>\n/// * Trading at: <https://trade.dydx.exchange/trade>\n/// * Rate Limits: <https://docs.dydx.exchange/#rate-limits>\n///   * 100 requests per 10 seconds\npub struct DydxSwapRestClient {\n    _api_key: Option<String>,\n    _api_secret: Option<String>,\n}\n\nimpl DydxSwapRestClient {\n    pub fn new(api_key: Option<String>, api_secret: Option<String>) -> Self {\n        DydxSwapRestClient { _api_key: api_key, _api_secret: api_secret }\n    }\n\n    /// Get a Level2 orderbook snapshot.\n    ///\n    /// All price levels are returned.\n    ///\n    /// For example: <https://api.dydx.exchange/v3/orderbook/BTC-USD>\n    pub fn fetch_l2_snapshot(symbol: &str) -> Result<String> {\n        gen_api!(format!(\"/v3/orderbook/{symbol}\"))\n    }\n\n    /// Get open interest.\n    ///\n    /// For example: <https://api.dydx.exchange/v3/markets>\n    pub fn fetch_open_interest() -> Result<String> {\n        gen_api!(\"/v3/markets\")\n    }\n}\n"
  },
  {
    "path": "crypto-rest-client/src/exchanges/dydx/mod.rs",
    "content": "pub(crate) mod dydx_swap;\n\nuse crate::error::Result;\nuse crypto_market_type::MarketType;\n\npub(crate) fn fetch_l2_snapshot(market_type: MarketType, symbol: &str) -> Result<String> {\n    let func = match market_type {\n        MarketType::LinearSwap => dydx_swap::DydxSwapRestClient::fetch_l2_snapshot,\n        _ => panic!(\"dYdX does not have the {market_type} market type\"),\n    };\n\n    func(symbol)\n}\n\npub(crate) fn fetch_open_interest(market_type: MarketType) -> Result<String> {\n    match market_type {\n        MarketType::InverseSwap | MarketType::LinearSwap => {\n            dydx_swap::DydxSwapRestClient::fetch_open_interest()\n        }\n        _ => panic!(\"dYdX {market_type} does not have open interest\"),\n    }\n}\n"
  },
  {
    "path": "crypto-rest-client/src/exchanges/ftx.rs",
    "content": "use super::utils::http_get;\nuse crate::error::Result;\nuse std::collections::BTreeMap;\n\nconst BASE_URL: &str = \"https://ftx.com/api\";\n\n/// The RESTful client for FTX.\n///\n/// FTX has Spot, LinearFuture, LinearSwap, Option, Move and BVOL markets.\n///\n/// * RESTful API doc: <https://docs.ftx.com/#rest-api>\n/// * Trading at <https://ftx.com/markets>\n/// * Rate Limits: <https://docs.ftx.com/?python#rate-limits>\n///   * Non-order placement requests do not count towards rate limits.\n///   * Rate limits are tiered by account trading volumes.\npub struct FtxRestClient {\n    _api_key: Option<String>,\n    _api_secret: Option<String>,\n}\n\nimpl FtxRestClient {\n    pub fn new(api_key: Option<String>, api_secret: Option<String>) -> Self {\n        FtxRestClient { _api_key: api_key, _api_secret: api_secret }\n    }\n\n    /// Get the latest Level2 snapshot of orderbook.\n    ///\n    /// Top 100 bids and asks are returned.\n    ///\n    /// For example: <https://ftx.com/api/markets/BTC-PERP/orderbook?depth=100>,\n    // <https://ftx.com/api/markets/BTC/USD/orderbook?depth=100>\n    pub fn fetch_l2_snapshot(symbol: &str) -> Result<String> {\n        gen_api!(format!(\"/markets/{symbol}/orderbook?depth=100\"))\n    }\n\n    /// Get open interest.\n    ///\n    /// For example:\n    /// - <https://ftx.com/api/futures>\n    pub fn fetch_open_interest() -> Result<String> {\n        gen_api!(\"/futures\")\n    }\n}\n"
  },
  {
    "path": "crypto-rest-client/src/exchanges/gate/gate_future.rs",
    "content": "use super::super::utils::http_get;\nuse crate::error::Result;\nuse std::collections::BTreeMap;\n\nconst BASE_URL: &str = \"https://api.gateio.ws/api/v4\";\n\n/// The RESTful client for Gate Future markets.\n///\n/// * RESTful API doc: <https://www.gate.io/docs/apiv4/en/index.html#delivery>\n/// * Trading at: <https://www.gateio.pro/cn/futures-delivery/usdt>\npub struct GateFutureRestClient {\n    _api_key: Option<String>,\n    _api_secret: Option<String>,\n}\n\nimpl GateFutureRestClient {\n    pub fn new(api_key: Option<String>, api_secret: Option<String>) -> Self {\n        GateFutureRestClient { _api_key: api_key, _api_secret: api_secret }\n    }\n\n    /// Get the latest Level2 snapshot of orderbook.\n    ///\n    /// Top 50 asks and bids are returned.\n    ///\n    /// For example:\n    ///\n    /// - <https://api.gateio.ws/api/v4/delivery/usdt/order_book?contract=BTC_USDT_20220624&limit=50>\n    /// - <https://api.gateio.ws/api/v4/delivery/btc/order_book?contract=BTC_USD_20220624&limit=50>\n    pub fn fetch_l2_snapshot(symbol: &str) -> Result<String> {\n        let without_date = &symbol[..(symbol.len() - 8)];\n        let settle = if without_date.ends_with(\"_USD_\") {\n            \"btc\"\n        } else if without_date.ends_with(\"_USDT_\") {\n            \"usdt\"\n        } else {\n            panic!(\"Unknown symbol {symbol}\");\n        };\n        gen_api!(format!(\"/delivery/{settle}/order_book?contract={symbol}&limit=50\"))\n    }\n}\n"
  },
  {
    "path": "crypto-rest-client/src/exchanges/gate/gate_spot.rs",
    "content": "use super::super::utils::http_get;\nuse crate::error::Result;\nuse std::collections::BTreeMap;\n\nconst BASE_URL: &str = \"https://api.gateio.ws/api/v4\";\n\n/// The RESTful client for Gate spot market.\n///\n/// * RESTful API doc: <https://www.gate.io/docs/apiv4/en/index.html>\n/// * Trading at: <https://www.gateio.pro/cn/trade/BTC_USDT>\n/// * Rate Limits: <https://www.gate.io/docs/apiv4/en/index.html#frequency-limit-rule>\n///   * 300 read operations per IP per second\npub struct GateSpotRestClient {\n    _api_key: Option<String>,\n    _api_secret: Option<String>,\n}\n\nimpl GateSpotRestClient {\n    pub fn new(api_key: Option<String>, api_secret: Option<String>) -> Self {\n        GateSpotRestClient { _api_key: api_key, _api_secret: api_secret }\n    }\n\n    /// Get the latest Level2 snapshot of orderbook.\n    ///\n    /// Top 1000 asks and bids are returned.\n    ///\n    /// For example: <https://api.gateio.ws/api/v4/spot/order_book?currency_pair=BTC_USDT&limit=1000>,\n    pub fn fetch_l2_snapshot(symbol: &str) -> Result<String> {\n        gen_api!(format!(\"/spot/order_book?currency_pair={symbol}&limit=1000\"))\n    }\n}\n"
  },
  {
    "path": "crypto-rest-client/src/exchanges/gate/gate_swap.rs",
    "content": "use super::super::utils::http_get;\nuse crate::error::Result;\nuse std::collections::BTreeMap;\n\nconst BASE_URL: &str = \"https://api.gateio.ws/api/v4\";\n\n/// The RESTful client for Gate Swap markets.\n///\n/// * RESTful API doc: <https://www.gate.io/docs/apiv4/en/index.html#futures>\n/// * Trading at: <https://www.gateio.pro/cn/futures_trade/USDT/BTC_USDT>\n/// * Rate Limits: <https://www.gate.io/docs/apiv4/en/index.html#frequency-limit-rule>\n///   * 300 read operations per IP per second\npub struct GateSwapRestClient {\n    _api_key: Option<String>,\n    _api_secret: Option<String>,\n}\n\nimpl GateSwapRestClient {\n    pub fn new(api_key: Option<String>, api_secret: Option<String>) -> Self {\n        GateSwapRestClient { _api_key: api_key, _api_secret: api_secret }\n    }\n\n    /// Get the latest Level2 snapshot of orderbook.\n    ///\n    /// Top 200 asks and bids are returned.\n    ///\n    /// For example:\n    ///\n    /// - <https://api.gateio.ws/api/v4/futures/btc/order_book?contract=BTC_USD&limit=200>\n    /// - <https://api.gateio.ws/api/v4/futures/usdt/order_book?contract=BTC_USDT&limit=200>\n    pub fn fetch_l2_snapshot(symbol: &str) -> Result<String> {\n        let settle = if symbol.ends_with(\"_USD\") {\n            \"btc\"\n        } else if symbol.ends_with(\"_USDT\") {\n            \"usdt\"\n        } else {\n            panic!(\"Unknown symbol {symbol}\");\n        };\n        gen_api!(format!(\"/futures/{settle}/order_book?contract={symbol}&limit=200\"))\n    }\n\n    /// Get open interest.\n    ///\n    /// For example:\n    /// - <https://api.gateio.ws/api/v4/futures/btc/contract_stats?contract=BTC_USD&interval=5m>\n    /// - <https://api.gateio.ws/api/v4/futures/usdt/contract_stats?contract=BTC_USDT&interval=5m>\n    pub fn fetch_open_interest(symbol: &str) -> Result<String> {\n        let settle = if symbol.ends_with(\"_USD\") {\n            \"btc\"\n        } else if symbol.ends_with(\"_USDT\") {\n            \"usdt\"\n        } else {\n            panic!(\"Unknown symbol {symbol}\");\n        };\n        gen_api!(format!(\"/futures/{settle}/contract_stats?contract={symbol}&interval=5m\"))\n    }\n}\n"
  },
  {
    "path": "crypto-rest-client/src/exchanges/gate/mod.rs",
    "content": "mod gate_future;\nmod gate_spot;\nmod gate_swap;\n\npub use gate_future::GateFutureRestClient;\npub use gate_spot::GateSpotRestClient;\npub use gate_swap::GateSwapRestClient;\n\nuse crate::error::Result;\nuse crypto_market_type::MarketType;\n\npub(crate) fn fetch_l2_snapshot(market_type: MarketType, symbol: &str) -> Result<String> {\n    let func = match market_type {\n        MarketType::Spot => gate_spot::GateSpotRestClient::fetch_l2_snapshot,\n        MarketType::InverseSwap | MarketType::LinearSwap => {\n            gate_swap::GateSwapRestClient::fetch_l2_snapshot\n        }\n        MarketType::InverseFuture | MarketType::LinearFuture => {\n            gate_future::GateFutureRestClient::fetch_l2_snapshot\n        }\n        _ => panic!(\"Gate unknown market_type: {market_type}\"),\n    };\n\n    func(symbol)\n}\n\npub(crate) fn fetch_open_interest(market_type: MarketType, symbol: &str) -> Result<String> {\n    let func = match market_type {\n        MarketType::InverseSwap | MarketType::LinearSwap => {\n            gate_swap::GateSwapRestClient::fetch_open_interest\n        }\n        _ => panic!(\"Gate {market_type} does NOT have open interest data\"),\n    };\n\n    func(symbol)\n}\n"
  },
  {
    "path": "crypto-rest-client/src/exchanges/huobi/huobi_future.rs",
    "content": "use super::super::utils::http_get;\nuse crate::error::Result;\nuse std::collections::BTreeMap;\n\nconst BASE_URL: &str = \"https://api.hbdm.com\";\n\n/// Huobi Future market.\n///\n/// * REST API doc: <https://huobiapi.github.io/docs/dm/v1/en/>\n/// * Trading at: <https://futures.huobi.com/en-us/contract/exchange/>\n/// * Rate Limits: <https://huobiapi.github.io/docs/dm/v1/en/#api-rate-limit-illustration>\n///   * For restful interfaces：all products(futures, coin margined swap, usdt\n///     margined swap ) 800 times/second for one IP at most\npub struct HuobiFutureRestClient {\n    _api_key: Option<String>,\n    _api_secret: Option<String>,\n}\n\nimpl_contract!(HuobiFutureRestClient);\n\nimpl HuobiFutureRestClient {\n    /// Get the latest Level2 orderbook snapshot.\n    ///\n    /// Top 150 bids and asks (aggregated) are returned.\n    ///\n    /// For example: <https://api.hbdm.com/market/depth?symbol=BTC_CQ&type=step0>\n    pub fn fetch_l2_snapshot(symbol: &str) -> Result<String> {\n        gen_api!(format!(\"/market/depth?symbol={symbol}&type=step0\"))\n    }\n\n    /// Get open interest.\n    ///\n    /// For example: <https://api.hbdm.com/api/v1/contract_open_interest?contract_code=BTC211231>\n    pub fn fetch_open_interest(symbol: Option<&str>) -> Result<String> {\n        if let Some(symbol) = symbol {\n            gen_api!(format!(\"/api/v1/contract_open_interest?contract_code={symbol}\"))\n        } else {\n            gen_api!(\"/api/v1/contract_open_interest\")\n        }\n    }\n}\n"
  },
  {
    "path": "crypto-rest-client/src/exchanges/huobi/huobi_inverse_swap.rs",
    "content": "use super::super::utils::http_get;\nuse crate::error::Result;\nuse std::collections::BTreeMap;\n\nconst BASE_URL: &str = \"https://api.hbdm.com\";\n\n/// Huobi Inverse Swap market.\n///\n/// Inverse Swap market uses coins like BTC as collateral.\n///\n/// * REST API doc: <https://huobiapi.github.io/docs/coin_margined_swap/v1/en/>\n/// * Trading at: <https://futures.huobi.com/en-us/swap/exchange/>\n/// * Rate Limits: <https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#api-rate-limit-illustration>\n///  * For restful interfaces：all products(futures, coin margined swap, usdt\n///    margined swap) 800 times/second for one IP at most\npub struct HuobiInverseSwapRestClient {\n    _api_key: Option<String>,\n    _api_secret: Option<String>,\n}\n\nimpl_contract!(HuobiInverseSwapRestClient);\n\nimpl HuobiInverseSwapRestClient {\n    /// Get the latest Level2 orderbook snapshot.\n    ///\n    /// Top 150 bids and asks (aggregated) are returned.\n    ///\n    /// For example: <https://api.hbdm.com/swap-ex/market/depth?contract_code=BTC-USD&type=step0>\n    pub fn fetch_l2_snapshot(symbol: &str) -> Result<String> {\n        gen_api!(format!(\"/swap-ex/market/depth?contract_code={symbol}&type=step0\"))\n    }\n\n    /// Get open interest.\n    ///\n    /// For example: <https://api.hbdm.com/swap-api/v1/swap_open_interest?contract_code=BTC-USD>\n    pub fn fetch_open_interest(symbol: Option<&str>) -> Result<String> {\n        if let Some(symbol) = symbol {\n            gen_api!(format!(\"/swap-api/v1/swap_open_interest?contract_code={symbol}\"))\n        } else {\n            gen_api!(\"/swap-api/v1/swap_open_interest\")\n        }\n    }\n}\n"
  },
  {
    "path": "crypto-rest-client/src/exchanges/huobi/huobi_linear_swap.rs",
    "content": "use super::super::utils::http_get;\nuse crate::error::Result;\nuse std::collections::BTreeMap;\n\nconst BASE_URL: &str = \"https://api.hbdm.com\";\n\n/// Huobi Linear Swap market.\n///\n/// Linear Swap market uses USDT as collateral.\n///\n/// * REST API doc: <https://huobiapi.github.io/docs/usdt_swap/v1/en/>\n/// * Trading at: <https://futures.huobi.com/en-us/linear_swap/exchange/>\n/// * Rate Limits: <https://huobiapi.github.io/docs/usdt_swap/v1/en/#api-rate-limit-illustration>\n///   * For restful interfaces, products, (future, coin margined swap, usdt\n///     margined swap)800 times/second for one IP at most\npub struct HuobiLinearSwapRestClient {\n    _api_key: Option<String>,\n    _api_secret: Option<String>,\n}\n\nimpl_contract!(HuobiLinearSwapRestClient);\n\nimpl HuobiLinearSwapRestClient {\n    /// Get the latest Level2 orderbook snapshot.\n    ///\n    /// Top 150 bids and asks (aggregated) are returned.\n    ///\n    /// For example: <https://api.hbdm.com/linear-swap-ex/market/depth?contract_code=BTC-USDT&type=step0>\n    pub fn fetch_l2_snapshot(symbol: &str) -> Result<String> {\n        gen_api!(format!(\"/linear-swap-ex/market/depth?contract_code={symbol}&type=step0\"))\n    }\n\n    /// Get open interest.\n    ///\n    /// For example: <https://api.hbdm.com/linear-swap-api/v1/swap_open_interest?contract_code=BTC-USDT>\n    pub fn fetch_open_interest(symbol: Option<&str>) -> Result<String> {\n        if let Some(symbol) = symbol {\n            gen_api!(format!(\"/linear-swap-api/v1/swap_open_interest?contract_code={symbol}\"))\n        } else {\n            gen_api!(\"/linear-swap-api/v1/swap_open_interest\")\n        }\n    }\n}\n"
  },
  {
    "path": "crypto-rest-client/src/exchanges/huobi/huobi_option.rs",
    "content": "use super::super::utils::http_get;\nuse crate::error::Result;\nuse std::collections::BTreeMap;\n\nconst BASE_URL: &str = \"https://api.hbdm.com/option-ex\";\n\n/// Huobi Option market.\n///\n///\n/// * REST API doc: <https://huobiapi.github.io/docs/option/v1/en/>\n/// * Trading at: <https://futures.huobi.com/en-us/option/exchange/>\n/// * Rate Limits: <https://huobiapi.github.io/docs/option/v1/en/#api-rate-limit-illustration>\n///   * For restful interfaces：all products(futures, coin margined swap, usdt\n///     margined swap and option) 800 times/second for one IP at most\npub struct HuobiOptionRestClient {\n    _api_key: Option<String>,\n    _api_secret: Option<String>,\n}\n\nimpl_contract!(HuobiOptionRestClient);\n\nimpl HuobiOptionRestClient {\n    /// Get the latest Level2 orderbook snapshot.\n    ///\n    /// Top 150 bids and asks (aggregated) are returned.\n    ///\n    /// For example: <https://api.hbdm.com/option-ex/market/depth?contract_code=BTC-USDT-210326-C-32000&type=step0>\n    pub fn fetch_l2_snapshot(symbol: &str) -> Result<String> {\n        gen_api!(format!(\"/market/depth?contract_code={symbol}&type=step0\"))\n    }\n}\n"
  },
  {
    "path": "crypto-rest-client/src/exchanges/huobi/huobi_spot.rs",
    "content": "use super::super::utils::http_get;\nuse crate::error::Result;\nuse std::collections::BTreeMap;\n\nconst BASE_URL: &str = \"https://api.huobi.pro\";\n\n/// Huobi Spot market.\n///\n/// * REST API doc: <https://huobiapi.github.io/docs/spot/v1/en/>\n/// * Trading at: <https://www.huobi.com/en-us/exchange/>\n/// * Rate Limits: <https://huobiapi.github.io/docs/spot/v1/en/#rate-limiting-rule>\n///   * If API Key is empty in request, then each IP is limited to 10 times per\n///     second\npub struct HuobiSpotRestClient {\n    _api_key: Option<String>,\n    _api_secret: Option<String>,\n}\n\nimpl_contract!(HuobiSpotRestClient);\n\nimpl HuobiSpotRestClient {\n    /// Get the latest Level2 orderbook snapshot.\n    ///\n    /// Top 150 bids and asks (aggregated) are returned.\n    ///\n    /// For example: <https://api.huobi.pro/market/depth?symbol=btcusdt&type=step0>\n    pub fn fetch_l2_snapshot(symbol: &str) -> Result<String> {\n        gen_api!(format!(\"/market/depth?symbol={symbol}&type=step0\"))\n    }\n}\n"
  },
  {
    "path": "crypto-rest-client/src/exchanges/huobi/mod.rs",
    "content": "#[macro_use]\nmod utils;\n\npub(crate) mod huobi_future;\npub(crate) mod huobi_inverse_swap;\npub(crate) mod huobi_linear_swap;\npub(crate) mod huobi_option;\npub(crate) mod huobi_spot;\n\nuse crate::error::{Error, Result};\nuse crypto_market_type::MarketType;\n\npub(crate) fn fetch_l2_snapshot(market_type: MarketType, symbol: &str) -> Result<String> {\n    let func = match market_type {\n        MarketType::Spot => huobi_spot::HuobiSpotRestClient::fetch_l2_snapshot,\n        MarketType::InverseFuture => huobi_future::HuobiFutureRestClient::fetch_l2_snapshot,\n        MarketType::LinearSwap => huobi_linear_swap::HuobiLinearSwapRestClient::fetch_l2_snapshot,\n        MarketType::InverseSwap => {\n            huobi_inverse_swap::HuobiInverseSwapRestClient::fetch_l2_snapshot\n        }\n        MarketType::EuropeanOption => huobi_option::HuobiOptionRestClient::fetch_l2_snapshot,\n        _ => panic!(\"Binance unknown market_type: {market_type}\"),\n    };\n\n    // if msg is {\"status\": \"maintain\"}, convert it to an error\n    match func(symbol) {\n        Ok(msg) => {\n            if msg == r#\"{\"status\": \"maintain\"}\"# {\n                Err(Error(msg))\n            } else {\n                Ok(msg)\n            }\n        }\n        Err(err) => Err(err),\n    }\n}\n\npub(crate) fn fetch_open_interest(market_type: MarketType, symbol: Option<&str>) -> Result<String> {\n    let func = match market_type {\n        MarketType::InverseFuture => huobi_future::HuobiFutureRestClient::fetch_open_interest,\n        MarketType::LinearSwap => huobi_linear_swap::HuobiLinearSwapRestClient::fetch_open_interest,\n        MarketType::InverseSwap => {\n            huobi_inverse_swap::HuobiInverseSwapRestClient::fetch_open_interest\n        }\n        _ => panic!(\"Huobi {market_type} does not have open interest\"),\n    };\n\n    // if msg is {\"status\": \"maintain\"}, convert it to an error\n    match func(symbol) {\n        Ok(msg) => {\n            if msg == r#\"{\"status\": \"maintain\"}\"# {\n                Err(Error(msg))\n            } else {\n                Ok(msg)\n            }\n        }\n        Err(err) => Err(err),\n    }\n}\n"
  },
  {
    "path": "crypto-rest-client/src/exchanges/huobi/utils.rs",
    "content": "macro_rules! impl_contract {\n    ($struct_name:ident) => {\n        impl $struct_name {\n            pub fn new(api_key: Option<String>, api_secret: Option<String>) -> Self {\n                Self { _api_key: api_key, _api_secret: api_secret }\n            }\n\n            /// Get the most recent trades.\n            ///\n            /// Equivalent to `/market/history/trade` with `size=2000`\n            ///\n            /// For example: <https://api.hbdm.com/market/history/trade?symbol=BTC_CQ&size=2000>\n            pub fn fetch_trades(symbol: &str) -> Result<String> {\n                gen_api!(format!(\"/market/history/trade?symbol={}&size=2000\", symbol))\n            }\n        }\n    };\n}\n"
  },
  {
    "path": "crypto-rest-client/src/exchanges/kraken/kraken_futures.rs",
    "content": "use super::super::utils::http_get;\nuse crate::error::Result;\nuse std::collections::BTreeMap;\n\n// see https://support.kraken.com/hc/en-us/articles/360022839491-API-URLs\nconst BASE_URL: &str = \"https://futures.kraken.com/derivatives/api/v3\";\n\n/// The WebSocket client for Kraken Futures.\n///\n/// * REST API doc: <https://support.kraken.com/hc/en-us/sections/360003562331-REST-API-Public>\n/// * Trading at: <https://futures.kraken.com/>\n/// * Rate Limits: <https://support.kraken.com/hc/en-us/articles/360022635612-Request-Limits-REST-API->\n///   * 500 every 10 seconds\npub struct KrakenFuturesRestClient {\n    _api_key: Option<String>,\n    _api_secret: Option<String>,\n}\n\nimpl KrakenFuturesRestClient {\n    pub fn new(api_key: Option<String>, api_secret: Option<String>) -> Self {\n        KrakenFuturesRestClient { _api_key: api_key, _api_secret: api_secret }\n    }\n\n    /// Get most recent trades.\n    ///\n    /// If `lastTime` is provided, return trade data from the specified\n    /// lastTime.\n    ///\n    /// For example: <https://futures.kraken.com/derivatives/api/v3/history?symbol=PI_XBTUSD>\n    #[allow(non_snake_case)]\n    pub fn fetch_trades(symbol: &str, lastTime: Option<String>) -> Result<String> {\n        gen_api!(format!(\"/history?symbol={symbol}\"), lastTime)\n    }\n\n    /// Get a Level2 snapshot of orderbook.\n    ///\n    /// For example: <https://futures.kraken.com/derivatives/api/v3/orderbook?symbol=PI_XBTUSD>\n    pub fn fetch_l2_snapshot(symbol: &str) -> Result<String> {\n        gen_api!(format!(\"/orderbook?symbol={symbol}\"))\n    }\n}\n"
  },
  {
    "path": "crypto-rest-client/src/exchanges/kraken/kraken_spot.rs",
    "content": "use super::super::utils::http_get;\nuse crate::error::Result;\nuse std::collections::BTreeMap;\n\nconst BASE_URL: &str = \"https://api.kraken.com\";\n\n/// The WebSocket client for Kraken.\n///\n/// Kraken has only Spot market.\n///\n/// * REST API doc: <https://docs.kraken.com/rest/>\n/// * Trading at: <https://trade.kraken.com/>\n/// * Rate Limits: <https://docs.kraken.com/rest/#section/Rate-Limits/REST-API-Rate-Limits>\n///   * 15 requests per 45 seconds\npub struct KrakenSpotRestClient {\n    _api_key: Option<String>,\n    _api_secret: Option<String>,\n}\n\nimpl KrakenSpotRestClient {\n    pub fn new(api_key: Option<String>, api_secret: Option<String>) -> Self {\n        KrakenSpotRestClient { _api_key: api_key, _api_secret: api_secret }\n    }\n\n    /// Get most recent trades.\n    ///\n    /// If `since` is provided, return trade data since given id (exclusive).\n    ///\n    /// For example: <https://api.kraken.com/0/public/Trades?pair=XXBTZUSD&since=1609893937598797338>\n    #[allow(non_snake_case)]\n    pub fn fetch_trades(symbol: &str, since: Option<String>) -> Result<String> {\n        if symbol.contains('/') {\n            // websocket and RESTful API have different symbol format\n            // XBT/USD -> XBTUSD\n            let stripped = symbol.replace('/', \"\");\n            gen_api!(format!(\"/0/public/Trades?pair={}\", &stripped), since)\n        } else {\n            gen_api!(format!(\"/0/public/Trades?pair={symbol}\"), since)\n        }\n    }\n\n    /// Get a Level2 snapshot of orderbook.\n    ///\n    /// Top 500 bids and asks are returned.\n    ///\n    /// For example: <https://api.kraken.com/0/public/Depth?pair=XXBTZUSD&count=500>\n    pub fn fetch_l2_snapshot(symbol: &str) -> Result<String> {\n        if symbol.contains('/') {\n            // websocket and RESTful API have different symbol format\n            // XBT/USD -> XBTUSD\n            let stripped = symbol.replace('/', \"\");\n            gen_api!(format!(\"/0/public/Depth?pair={stripped}&count=500\"))\n        } else {\n            gen_api!(format!(\"/0/public/Depth?pair={symbol}&count=500\"))\n        }\n    }\n}\n"
  },
  {
    "path": "crypto-rest-client/src/exchanges/kraken/mod.rs",
    "content": "pub(crate) mod kraken_futures;\npub(crate) mod kraken_spot;\n\nuse crate::error::Result;\nuse crypto_market_type::MarketType;\n\npub(crate) fn fetch_l2_snapshot(market_type: MarketType, symbol: &str) -> Result<String> {\n    let func = match market_type {\n        MarketType::Spot => kraken_spot::KrakenSpotRestClient::fetch_l2_snapshot,\n        MarketType::InverseFuture | MarketType::InverseSwap => {\n            kraken_futures::KrakenFuturesRestClient::fetch_l2_snapshot\n        }\n        _ => panic!(\"Kraken unknown market_type: {market_type}\"),\n    };\n\n    func(symbol)\n}\n"
  },
  {
    "path": "crypto-rest-client/src/exchanges/kucoin/kucoin_spot.rs",
    "content": "use super::super::utils::http_get;\nuse crate::error::Result;\nuse std::collections::BTreeMap;\n\nconst BASE_URL: &str = \"https://api.kucoin.com\";\n\n/// The RESTful client for KuCoin spot market.\n///\n/// * RESTful API doc: <https://docs.kucoin.com/>\n/// * Trading at: <https://trade.kucoin.com/>\n/// * Rate Limits: <https://docs.kucoin.com/#request-rate-limit>\npub struct KuCoinSpotRestClient {\n    _api_key: Option<String>,\n    _api_secret: Option<String>,\n}\n\nimpl KuCoinSpotRestClient {\n    pub fn new(api_key: Option<String>, api_secret: Option<String>) -> Self {\n        KuCoinSpotRestClient { _api_key: api_key, _api_secret: api_secret }\n    }\n\n    /// Get the latest Level2 snapshot of orderbook.\n    ///\n    /// For example: <https://api.kucoin.com/api/v1/market/orderbook/level2_100?symbol=BTC-USDT>,\n    pub fn fetch_l2_snapshot(symbol: &str) -> Result<String> {\n        let api = if std::env::var(\"KC-API-KEY\").is_ok() {\n            // the request rate limit is 30 times/3s\n            \"/api/v3/market/orderbook/level2\"\n        } else {\n            \"/api/v1/market/orderbook/level2_100\"\n        };\n        gen_api!(format!(\"{api}?symbol={symbol}\"))\n    }\n\n    /// Get the latest Level3 snapshot of orderbook.\n    ///\n    /// All bids and asks are returned.\n    ///\n    /// For example: <https://api.kucoin.com/api/v2/market/orderbook/level3?symbol=BTC-USDT>,\n    pub fn fetch_l3_snapshot(symbol: &str) -> Result<String> {\n        gen_api!(format!(\"/api/v2/market/orderbook/level3?symbol={symbol}\"))\n    }\n}\n"
  },
  {
    "path": "crypto-rest-client/src/exchanges/kucoin/kucoin_swap.rs",
    "content": "use super::super::utils::http_get;\nuse crate::error::Result;\nuse std::collections::BTreeMap;\n\nconst BASE_URL: &str = \"https://api-futures.kucoin.com\";\n\n/// The RESTful client for KuCoin Future and Swap markets.\n///\n/// * RESTful API doc: <https://docs.kucoin.com/futures>\n/// * Trading at: <https://futures.kucoin.com/>\n/// * Rate Limits: <https://docs.kucoin.cc/futures/#request-rate-limit>\npub struct KuCoinSwapRestClient {\n    _api_key: Option<String>,\n    _api_secret: Option<String>,\n}\n\nimpl KuCoinSwapRestClient {\n    pub fn new(api_key: Option<String>, api_secret: Option<String>) -> Self {\n        KuCoinSwapRestClient { _api_key: api_key, _api_secret: api_secret }\n    }\n\n    /// Get the latest Level2 snapshot of orderbook.\n    ///\n    /// All bids and asks are returned.\n    ///\n    /// For example: <https://api-futures.kucoin.com/api/v1/level2/snapshot?symbol=XBTUSDM>,\n    pub fn fetch_l2_snapshot(symbol: &str) -> Result<String> {\n        // the request rate limit is 30 times/3s\n        gen_api!(format!(\"/api/v1/level2/snapshot?symbol={symbol}\"))\n    }\n\n    /// Get the latest Level3 snapshot of orderbook.\n    ///\n    /// All bids and asks are returned.\n    ///\n    /// For example: <https://api-futures.kucoin.com/api/v2/level3/snapshot?symbol=XBTUSDM>,\n    pub fn fetch_l3_snapshot(symbol: &str) -> Result<String> {\n        gen_api!(format!(\"/api/v2/level3/snapshot?symbol={symbol}\"))\n    }\n\n    /// Get open interest.\n    ///\n    /// For example:\n    /// - <https://ftx.com/api/futures>\n    pub fn fetch_open_interest() -> Result<String> {\n        gen_api!(\"/api/v1/contracts/active\")\n    }\n}\n"
  },
  {
    "path": "crypto-rest-client/src/exchanges/kucoin/mod.rs",
    "content": "mod kucoin_spot;\nmod kucoin_swap;\n\npub use kucoin_spot::KuCoinSpotRestClient;\npub use kucoin_swap::KuCoinSwapRestClient;\n\nuse crate::error::Result;\nuse crypto_market_type::MarketType;\n\npub(crate) fn fetch_l2_snapshot(market_type: MarketType, symbol: &str) -> Result<String> {\n    let func = match market_type {\n        MarketType::Spot => kucoin_spot::KuCoinSpotRestClient::fetch_l2_snapshot,\n        MarketType::InverseSwap | MarketType::LinearSwap | MarketType::InverseFuture => {\n            kucoin_swap::KuCoinSwapRestClient::fetch_l2_snapshot\n        }\n        _ => panic!(\"Bitget unknown market_type: {market_type}\"),\n    };\n\n    func(symbol)\n}\n\npub(crate) fn fetch_l3_snapshot(market_type: MarketType, symbol: &str) -> Result<String> {\n    let func = match market_type {\n        MarketType::Spot => kucoin_spot::KuCoinSpotRestClient::fetch_l3_snapshot,\n        MarketType::InverseSwap | MarketType::LinearSwap | MarketType::InverseFuture => {\n            kucoin_swap::KuCoinSwapRestClient::fetch_l3_snapshot\n        }\n        _ => panic!(\"Bitget unknown market_type: {market_type}\"),\n    };\n\n    func(symbol)\n}\n\npub(crate) fn fetch_open_interest(market_type: MarketType) -> Result<String> {\n    match market_type {\n        MarketType::InverseSwap | MarketType::LinearSwap | MarketType::Unknown => {\n            kucoin_swap::KuCoinSwapRestClient::fetch_open_interest()\n        }\n        _ => panic!(\"kucoin {market_type} does not have open interest\"),\n    }\n}\n"
  },
  {
    "path": "crypto-rest-client/src/exchanges/mexc/mexc_spot.rs",
    "content": "use super::super::utils::http_get;\nuse crate::error::Result;\nuse std::collections::BTreeMap;\n\nconst BASE_URL: &str = \"https://www.mexc.com\";\n\n/// MEXC Spot market.\n///\n/// * REST API doc: <https://mxcdevelop.github.io/APIDoc/>\n/// * Trading at: <https://www.mexc.com/exchange/BTC_USDT>\n/// * Rate Limits: <https://mxcdevelop.github.io/APIDoc/open.api.v2.en.html#rate-limit>\n///   * The default rate limiting rule for an endpoint is 20 times per second.\npub struct MexcSpotRestClient {\n    _access_key: String,\n    _secret_key: Option<String>,\n}\n\nimpl MexcSpotRestClient {\n    pub fn new(access_key: String, secret_key: Option<String>) -> Self {\n        MexcSpotRestClient { _access_key: access_key, _secret_key: secret_key }\n    }\n\n    /// Get latest trades.\n    ///\n    /// 1000 trades are returned.\n    ///\n    /// For example: <https://www.mexc.com/open/api/v2/market/deals?symbol=BTC_USDT&limit=1000>\n    #[allow(non_snake_case)]\n    pub fn fetch_trades(symbol: &str) -> Result<String> {\n        gen_api!(format!(\"/open/api/v2/market/deals?symbol={symbol}&limit=1000\"))\n    }\n\n    /// Get latest Level2 snapshot of orderbook.\n    ///\n    /// Top 2000 bids and asks will be returned.\n    ///\n    /// For example: <https://www.mexc.com/open/api/v2/market/depth?symbol=BTC_USDT&depth=2000>\n    pub fn fetch_l2_snapshot(symbol: &str) -> Result<String> {\n        gen_api!(format!(\"/open/api/v2/market/depth?symbol={symbol}&depth=2000\"))\n    }\n}\n"
  },
  {
    "path": "crypto-rest-client/src/exchanges/mexc/mexc_swap.rs",
    "content": "use super::super::utils::http_get;\nuse crate::error::Result;\nuse std::collections::BTreeMap;\n\nconst BASE_URL: &str = \"https://contract.mexc.com\";\n\n/// MEXC Swap market.\n///\n/// * REST API doc: <https://mxcdevelop.github.io/APIDoc/>\n/// * Trading at: <https://contract.mexc.com/exchange>\npub struct MexcSwapRestClient {\n    _api_key: Option<String>,\n    _api_secret: Option<String>,\n}\n\nimpl MexcSwapRestClient {\n    pub fn new(api_key: Option<String>, api_secret: Option<String>) -> Self {\n        MexcSwapRestClient { _api_key: api_key, _api_secret: api_secret }\n    }\n\n    /// Get most recent trades.\n    ///\n    /// For example: <https://contract.mexc.com/api/v1/contract/deals/BTC_USDT>\n    pub fn fetch_trades(symbol: &str) -> Result<String> {\n        gen_api!(format!(\"/api/v1/contract/deals/{symbol}\"))\n    }\n\n    /// Get the latest Level2 snapshot of orderbook.\n    ///\n    /// Top 2000 bids and asks will be returned.\n    ///\n    /// For example: <https://contract.mexc.com/api/v1/contract/depth/BTC_USDT?limit=2000>\n    ///\n    /// Rate limit: 20 times /2 seconds\n    pub fn fetch_l2_snapshot(symbol: &str) -> Result<String> {\n        gen_api!(format!(\"/api/v1/contract/depth/{symbol}?limit=2000\"))\n    }\n}\n"
  },
  {
    "path": "crypto-rest-client/src/exchanges/mexc/mod.rs",
    "content": "pub(crate) mod mexc_spot;\npub(crate) mod mexc_swap;\n\nuse crate::error::Result;\nuse crypto_market_type::MarketType;\n\npub(crate) fn fetch_l2_snapshot(market_type: MarketType, symbol: &str) -> Result<String> {\n    let func = match market_type {\n        MarketType::Spot => mexc_spot::MexcSpotRestClient::fetch_l2_snapshot,\n        MarketType::InverseSwap | MarketType::LinearSwap => {\n            mexc_swap::MexcSwapRestClient::fetch_l2_snapshot\n        }\n        _ => panic!(\"MEXC unknown market_type: {market_type}\"),\n    };\n\n    func(symbol)\n}\n"
  },
  {
    "path": "crypto-rest-client/src/exchanges/mod.rs",
    "content": "#[macro_use]\nmod utils;\n\npub(super) mod binance;\npub(super) mod bitfinex;\npub(super) mod bitget;\npub(super) mod bithumb;\npub(super) mod bitmex;\npub(super) mod bitstamp;\npub(super) mod bitz;\npub(super) mod bybit;\npub(super) mod coinbase_pro;\npub(super) mod deribit;\npub(super) mod dydx;\npub(super) mod ftx;\npub(super) mod gate;\npub(super) mod huobi;\npub(super) mod kraken;\npub(super) mod kucoin;\npub(super) mod mexc;\npub(super) mod okx;\npub(super) mod zb;\npub(super) mod zbg;\n"
  },
  {
    "path": "crypto-rest-client/src/exchanges/okx.rs",
    "content": "use super::utils::http_get;\nuse crate::error::Result;\nuse crypto_market_type::MarketType;\nuse serde_json::Value;\nuse std::collections::{BTreeMap, HashMap};\n\nconst BASE_URL: &str = \"https://www.okx.com\";\n\n/// The REST client for OKEx.\n///\n/// OKEx has Spot, Future, Swap and Option markets.\n///\n/// * API doc: <https://www.okx.com/docs-v5/en/>\n/// * Trading at:\n///     * Spot <https://www.okx.com/trade-spot>\n///     * Future <https://www.okx.com/trade-futures>\n///     * Swap <https://www.okx.com/trade-swap>\n///     * Option <https://www.okx.com/trade-option>\npub struct OkxRestClient {\n    _api_key: Option<String>,\n    _api_secret: Option<String>,\n}\n\nimpl OkxRestClient {\n    pub fn new(api_key: Option<String>, api_secret: Option<String>) -> Self {\n        OkxRestClient { _api_key: api_key, _api_secret: api_secret }\n    }\n\n    /// Get most recent trades.\n    ///\n    /// 500 trades are returned.\n    ///\n    /// For example: <https://www.okx.com/api/v5/market/trades?instId=BTC-USDT&limit=500>\n    pub fn fetch_trades(symbol: &str) -> Result<String> {\n        gen_api!(format!(\"/api/v5/market/trades?instId={symbol}&limit=500\"))\n    }\n\n    /// Get the latest Level2 snapshot of orderbook.\n    ///\n    /// Top 400 bids and asks are returned.\n    ///\n    /// For example:\n    /// * <https://www.okx.com/api/v5/market/books?instId=BTC-USDT&sz=400>,\n    /// * <https://www.okx.com/api/v5/market/books?instId=BTC-USDT-SWAP&sz=400>\n    ///\n    /// Rate limit: 20 requests per 2 seconds\n    pub fn fetch_l2_snapshot(symbol: &str) -> Result<String> {\n        gen_api!(format!(\"/api/v5/market/books?instId={symbol}&sz=400\",))\n    }\n\n    /// Get option underlying.\n    pub fn fetch_option_underlying() -> Result<Vec<String>> {\n        let txt = http_get(\n            \"https://www.okx.com/api/v5/public/underlying?instType=OPTION\",\n            &BTreeMap::new(),\n        )?;\n        let json_obj = serde_json::from_str::<HashMap<String, Value>>(&txt).unwrap();\n        let data = json_obj.get(\"data\").unwrap().as_array().unwrap()[0].as_array().unwrap();\n        let underlying_indexes =\n            data.iter().map(|x| x.as_str().unwrap().to_string()).collect::<Vec<String>>();\n        Ok(underlying_indexes)\n    }\n\n    /// Get open interest.\n    ///\n    /// inst_type: SWAP, FUTURES, OPTION\n    ///\n    /// For example:\n    /// - <https://www.okx.com/api/v5/public/open-interest?instType=SWAP>\n    /// - <https://www.okx.com/api/v5/public/open-interest?instType=SWAP&instId=BTC-USD-SWAP>\n    pub fn fetch_open_interest(market_type: MarketType, symbol: Option<&str>) -> Result<String> {\n        let inst_type = match market_type {\n            MarketType::LinearFuture => \"FUTURES\",\n            MarketType::InverseFuture => \"FUTURES\",\n            MarketType::LinearSwap => \"SWAP\",\n            MarketType::InverseSwap => \"SWAP\",\n            MarketType::EuropeanOption => \"OPTION\",\n            _ => panic!(\"okx {market_type} doesn't have open interest\"),\n        };\n        if let Some(inst_id) = symbol {\n            gen_api!(format!(\"/api/v5/public/open-interest?instType={inst_type}&instId={inst_id}\",))\n        } else {\n            gen_api!(format!(\"/api/v5/public/open-interest?instType={inst_type}\"))\n        }\n    }\n}\n"
  },
  {
    "path": "crypto-rest-client/src/exchanges/utils.rs",
    "content": "use reqwest::{blocking::Response, header};\n\nuse crate::error::{Error, Result};\nuse std::collections::BTreeMap;\n\n// Returns the raw response directly.\npub(super) fn http_get_raw(url: &str, params: &BTreeMap<String, String>) -> Result<Response> {\n    let mut full_url = url.to_string();\n    let mut first = true;\n    for (k, v) in params.iter() {\n        if first {\n            full_url.push_str(format!(\"?{k}={v}\").as_str());\n            first = false;\n        } else {\n            full_url.push_str(format!(\"&{k}={v}\").as_str());\n        }\n    }\n    // println!(\"{}\", full_url);\n\n    let mut headers = header::HeaderMap::new();\n    headers.insert(header::CONTENT_TYPE, header::HeaderValue::from_static(\"application/json\"));\n\n    let client = reqwest::blocking::Client::builder()\n         .default_headers(headers)\n         .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\")\n         .gzip(true)\n         .build()?;\n    let response = client.get(full_url.as_str()).send()?;\n    Ok(response)\n}\n\n// Returns the text in response.\npub(super) fn http_get(url: &str, params: &BTreeMap<String, String>) -> Result<String> {\n    match http_get_raw(url, params) {\n        Ok(response) => match response.error_for_status() {\n            Ok(resp) => Ok(resp.text()?),\n            Err(error) => Err(Error::from(error)),\n        },\n        Err(err) => Err(err),\n    }\n}\n\nmacro_rules! gen_api {\n    ( $path:expr$(, $param_name:ident )* ) => {\n        {\n            #[allow(unused_mut)]\n            let mut params = BTreeMap::new();\n            $(\n                if let Some(param_name) = $param_name {\n                    params.insert(stringify!($param_name).to_string(), param_name.to_string());\n                }\n            )*\n            let url = if $path.starts_with(\"http\") { $path.to_string() } else { format!(\"{}{}\",BASE_URL, $path) };\n            http_get(&url, &params)\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use std::collections::BTreeMap;\n\n    use serde_json::Value;\n\n    // System proxies are enabled by default, see <https://docs.rs/reqwest/latest/reqwest/#proxies>\n    #[test]\n    #[ignore]\n    fn use_system_socks_proxy() {\n        std::env::set_var(\"https_proxy\", \"socks5://127.0.0.1:9050\");\n        let text =\n            super::http_get(\"https://check.torproject.org/api/ip\", &BTreeMap::new()).unwrap();\n        let obj = serde_json::from_str::<BTreeMap<String, Value>>(&text).unwrap();\n        assert!(obj.get(\"IsTor\").unwrap().as_bool().unwrap());\n    }\n\n    #[test]\n    #[ignore]\n    fn use_system_https_proxy() {\n        std::env::set_var(\"https_proxy\", \"http://127.0.0.1:8118\");\n        let text =\n            super::http_get(\"https://check.torproject.org/api/ip\", &BTreeMap::new()).unwrap();\n        let obj = serde_json::from_str::<BTreeMap<String, Value>>(&text).unwrap();\n        assert!(obj.get(\"IsTor\").unwrap().as_bool().unwrap());\n    }\n}\n"
  },
  {
    "path": "crypto-rest-client/src/exchanges/zb/mod.rs",
    "content": "mod zb_spot;\nmod zb_swap;\n\npub use zb_spot::ZbSpotRestClient;\npub use zb_swap::ZbSwapRestClient;\n\nuse crate::error::Result;\nuse crypto_market_type::MarketType;\n\npub(crate) fn fetch_l2_snapshot(market_type: MarketType, symbol: &str) -> Result<String> {\n    let func = match market_type {\n        MarketType::Spot => zb_spot::ZbSpotRestClient::fetch_l2_snapshot,\n        MarketType::InverseSwap | MarketType::LinearSwap => {\n            zb_swap::ZbSwapRestClient::fetch_l2_snapshot\n        }\n        _ => panic!(\"ZBG unknown market_type: {market_type}\"),\n    };\n\n    func(symbol)\n}\n"
  },
  {
    "path": "crypto-rest-client/src/exchanges/zb/zb_spot.rs",
    "content": "use super::super::utils::http_get;\nuse crate::error::Result;\nuse std::collections::BTreeMap;\n\nconst BASE_URL: &str = \"https://api.zb.com\";\n\n/// The RESTful client for ZB spot market.\n///\n/// * RESTful API doc: <https://www.zb.com/en/api>\n/// * Trading at: <https://www.zb.com/en/kline/btc_usdt>\npub struct ZbSpotRestClient {\n    _api_key: Option<String>,\n    _api_secret: Option<String>,\n}\n\nimpl ZbSpotRestClient {\n    pub fn new(api_key: Option<String>, api_secret: Option<String>) -> Self {\n        ZbSpotRestClient { _api_key: api_key, _api_secret: api_secret }\n    }\n\n    /// Get the latest Level2 snapshot of orderbook.\n    ///\n    /// Top 50 bids and asks are returned.\n    ///\n    /// For example: <https://api.zbex.site/data/v1/depth?market=btc_usdt&size=50>,\n    pub fn fetch_l2_snapshot(symbol: &str) -> Result<String> {\n        gen_api!(format!(\"/data/v1/depth?market={symbol}&size=50\"))\n    }\n}\n"
  },
  {
    "path": "crypto-rest-client/src/exchanges/zb/zb_swap.rs",
    "content": "use super::super::utils::http_get;\nuse crate::error::Result;\nuse std::collections::BTreeMap;\n\nconst BASE_URL: &str = \"https://fapi.zb.com\";\n\n/// The RESTful client for ZB swap markets.\n///\n/// * RESTful API doc: <https://www.zb.com/en/contract-api>\n/// * Trading at: <https://www.zb.com/en/futures/btc_usdt>\npub struct ZbSwapRestClient {\n    _api_key: Option<String>,\n    _api_secret: Option<String>,\n}\n\nimpl ZbSwapRestClient {\n    pub fn new(api_key: Option<String>, api_secret: Option<String>) -> Self {\n        ZbSwapRestClient { _api_key: api_key, _api_secret: api_secret }\n    }\n\n    /// Get the latest Level2 snapshot of orderbook.\n    ///\n    /// Top 200 bids and asks are returned.\n    ///\n    /// For example:\n    /// * <https://fapi.zb.com/api/public/v1/depth?symbol=BTC_USDT&size=200>\n    /// * <https://fapi.zb.com/qc/api/public/v1/depth?symbol=BTC_QC&size=200>\n    pub fn fetch_l2_snapshot(symbol: &str) -> Result<String> {\n        if symbol.ends_with(\"_QC\") {\n            gen_api!(format!(\"/qc/api/public/v1/depth?symbol={symbol}&size=200\"))\n        } else {\n            gen_api!(format!(\"/api/public/v1/depth?symbol={symbol}&size=200\"))\n        }\n    }\n}\n"
  },
  {
    "path": "crypto-rest-client/src/exchanges/zbg/mod.rs",
    "content": "mod zbg_spot;\nmod zbg_swap;\n\npub use zbg_spot::ZbgSpotRestClient;\npub use zbg_swap::ZbgSwapRestClient;\n\nuse crate::error::Result;\nuse crypto_market_type::MarketType;\n\npub(crate) fn fetch_l2_snapshot(market_type: MarketType, symbol: &str) -> Result<String> {\n    let func = match market_type {\n        MarketType::Spot => zbg_spot::ZbgSpotRestClient::fetch_l2_snapshot,\n        MarketType::InverseSwap | MarketType::LinearSwap => {\n            zbg_swap::ZbgSwapRestClient::fetch_l2_snapshot\n        }\n        _ => panic!(\"ZBG unknown market_type: {market_type}\"),\n    };\n\n    func(symbol)\n}\n\npub(crate) fn fetch_open_interest(market_type: MarketType, symbol: &str) -> Result<String> {\n    let func = match market_type {\n        MarketType::InverseSwap | MarketType::LinearSwap => {\n            zbg_swap::ZbgSwapRestClient::fetch_open_interest\n        }\n        _ => panic!(\"ZBG {market_type} does NOT have open interest data\"),\n    };\n\n    func(symbol)\n}\n"
  },
  {
    "path": "crypto-rest-client/src/exchanges/zbg/zbg_spot.rs",
    "content": "use super::super::utils::http_get;\nuse crate::error::Result;\nuse std::collections::BTreeMap;\n\nconst BASE_URL: &str = \"https://kline.zbg.com\";\n\n/// The RESTful client for ZBG spot market.\n///\n/// * RESTful API doc: <https://zbgapi.github.io/docs/spot/v1/en/>\n/// * Trading at: <https://www.zbg.com/trade/>\npub struct ZbgSpotRestClient {\n    _api_key: Option<String>,\n    _api_secret: Option<String>,\n}\n\nimpl ZbgSpotRestClient {\n    pub fn new(api_key: Option<String>, api_secret: Option<String>) -> Self {\n        ZbgSpotRestClient { _api_key: api_key, _api_secret: api_secret }\n    }\n\n    /// Get the latest Level2 snapshot of orderbook.\n    ///\n    /// Top 200 bids and asks are returned.\n    ///\n    /// For example: <https://kline.zbg.com/api/data/v1/entrusts?marketName=btc_usdt&dataSize=200>,\n    pub fn fetch_l2_snapshot(symbol: &str) -> Result<String> {\n        gen_api!(format!(\"/api/data/v1/entrusts?marketName={symbol}&dataSize=200\"))\n    }\n}\n"
  },
  {
    "path": "crypto-rest-client/src/exchanges/zbg/zbg_swap.rs",
    "content": "use super::super::utils::http_get;\nuse crate::error::Result;\nuse std::collections::BTreeMap;\n\nconst BASE_URL: &str = \"https://www.zbg.com\";\n\n/// The RESTful client for ZBG swap markets.\n///\n/// * RESTful API doc: <https://zbgapi.github.io/docs/future/v1/en/>\n/// * Trading at: <https://futures.zbg.com/>\npub struct ZbgSwapRestClient {\n    _api_key: Option<String>,\n    _api_secret: Option<String>,\n}\n\nimpl ZbgSwapRestClient {\n    pub fn new(api_key: Option<String>, api_secret: Option<String>) -> Self {\n        ZbgSwapRestClient { _api_key: api_key, _api_secret: api_secret }\n    }\n\n    /// Get the latest Level2 snapshot of orderbook.\n    ///\n    /// Top 200 bids and asks are returned.\n    ///\n    /// For example: <https://www.zbg.com/exchange/api/v1/future/market/depth?symbol=BTC_USD-R&size=200>\n    pub fn fetch_l2_snapshot(symbol: &str) -> Result<String> {\n        gen_api!(format!(\"/exchange/api/v1/future/market/depth?symbol={symbol}&size=1000\"))\n    }\n\n    /// Get open interest.\n    ///\n    /// For example:\n    ///\n    /// - <https://www.zbg.com/exchange/api/v1/future/market/ticker?symbol=BTC_USD-R>\n    pub fn fetch_open_interest(symbol: &str) -> Result<String> {\n        gen_api!(format!(\"/exchange/api/v1/future/market/ticker?symbol={symbol}\"))\n    }\n}\n"
  },
  {
    "path": "crypto-rest-client/src/lib.rs",
    "content": "mod error;\nmod exchanges;\n\npub use error::Error;\npub use exchanges::{\n    binance::{\n        binance_inverse::BinanceInverseRestClient, binance_linear::BinanceLinearRestClient,\n        binance_option::BinanceOptionRestClient, binance_spot::BinanceSpotRestClient,\n    },\n    bitfinex::BitfinexRestClient,\n    bitget::*,\n    bithumb::*,\n    bitmex::BitmexRestClient,\n    bitstamp::BitstampRestClient,\n    bitz::*,\n    bybit::BybitRestClient,\n    coinbase_pro::CoinbaseProRestClient,\n    deribit::DeribitRestClient,\n    dydx::dydx_swap::DydxSwapRestClient,\n    ftx::FtxRestClient,\n    gate::*,\n    huobi::{\n        huobi_future::HuobiFutureRestClient, huobi_inverse_swap::HuobiInverseSwapRestClient,\n        huobi_linear_swap::HuobiLinearSwapRestClient, huobi_option::HuobiOptionRestClient,\n        huobi_spot::HuobiSpotRestClient,\n    },\n    kraken::{kraken_futures::KrakenFuturesRestClient, kraken_spot::KrakenSpotRestClient},\n    kucoin::*,\n    mexc::{mexc_spot::MexcSpotRestClient, mexc_swap::MexcSwapRestClient},\n    okx::OkxRestClient,\n    zb::*,\n    zbg::*,\n};\n\nuse crypto_market_type::MarketType;\nuse error::Result;\nuse log::*;\nuse std::time::{Duration, SystemTime};\n\nfn fetch_l2_snapshot_internal(\n    exchange: &str,\n    market_type: MarketType,\n    symbol: &str,\n) -> Result<String> {\n    let ret = match exchange {\n        \"binance\" => exchanges::binance::fetch_l2_snapshot(market_type, symbol),\n        \"bitfinex\" => exchanges::bitfinex::BitfinexRestClient::fetch_l2_snapshot(symbol),\n        \"bitget\" => exchanges::bitget::fetch_l2_snapshot(market_type, symbol),\n        \"bithumb\" => exchanges::bithumb::BithumbRestClient::fetch_l2_snapshot(symbol),\n        \"bitmex\" => exchanges::bitmex::BitmexRestClient::fetch_l2_snapshot(symbol),\n        \"bitstamp\" => exchanges::bitstamp::BitstampRestClient::fetch_l2_snapshot(symbol),\n        \"bitz\" => exchanges::bitz::fetch_l2_snapshot(market_type, symbol),\n        \"bybit\" => exchanges::bybit::BybitRestClient::fetch_l2_snapshot(symbol),\n        \"coinbase_pro\" => exchanges::coinbase_pro::CoinbaseProRestClient::fetch_l2_snapshot(symbol),\n        \"deribit\" => exchanges::deribit::DeribitRestClient::fetch_l2_snapshot(symbol),\n        \"dydx\" => exchanges::dydx::fetch_l2_snapshot(market_type, symbol),\n        \"ftx\" => exchanges::ftx::FtxRestClient::fetch_l2_snapshot(symbol),\n        \"gate\" => exchanges::gate::fetch_l2_snapshot(market_type, symbol),\n        \"huobi\" => exchanges::huobi::fetch_l2_snapshot(market_type, symbol),\n        \"kraken\" => exchanges::kraken::fetch_l2_snapshot(market_type, symbol),\n        \"kucoin\" => exchanges::kucoin::fetch_l2_snapshot(market_type, symbol),\n        \"mexc\" => exchanges::mexc::fetch_l2_snapshot(market_type, symbol),\n        \"okx\" => exchanges::okx::OkxRestClient::fetch_l2_snapshot(symbol),\n        \"zb\" => exchanges::zb::fetch_l2_snapshot(market_type, symbol),\n        \"zbg\" => exchanges::zbg::fetch_l2_snapshot(market_type, symbol),\n        _ => panic!(\"Unknown exchange {exchange}\"),\n    };\n    match ret {\n        Ok(s) => Ok(s.trim().to_string()),\n        Err(_) => ret,\n    }\n}\n\npub fn fetch_l3_snapshot_internal(\n    exchange: &str,\n    market_type: MarketType,\n    symbol: &str,\n) -> Result<String> {\n    let ret = match exchange {\n        \"bitfinex\" => exchanges::bitfinex::BitfinexRestClient::fetch_l3_snapshot(symbol),\n        \"bitstamp\" => exchanges::bitstamp::BitstampRestClient::fetch_l3_snapshot(symbol),\n        \"coinbase_pro\" => exchanges::coinbase_pro::CoinbaseProRestClient::fetch_l3_snapshot(symbol),\n        \"kucoin\" => exchanges::kucoin::fetch_l3_snapshot(market_type, symbol),\n        _ => panic!(\"{exchange} {market_type} does NOT provide level3 orderbook data\"),\n    };\n    match ret {\n        Ok(s) => Ok(s.trim().to_string()),\n        Err(_) => ret,\n    }\n}\n\n/// Fetch open interest.\n///\n/// `symbol` None means fetch all symbols.\npub fn fetch_open_interest(\n    exchange: &str,\n    market_type: MarketType,\n    symbol: Option<&str>,\n) -> Result<String> {\n    let ret = match exchange {\n        \"binance\" => exchanges::binance::fetch_open_interest(market_type, symbol.unwrap()),\n        \"bitget\" => exchanges::bitget::fetch_open_interest(market_type, symbol.unwrap()),\n        \"bybit\" => exchanges::bybit::BybitRestClient::fetch_open_interest(symbol.unwrap()),\n        \"bitz\" => exchanges::bitz::fetch_open_interest(market_type, symbol),\n        \"deribit\" => exchanges::deribit::DeribitRestClient::fetch_open_interest(symbol),\n        \"dydx\" => exchanges::dydx::fetch_open_interest(market_type),\n        \"ftx\" => exchanges::ftx::FtxRestClient::fetch_open_interest(),\n        \"gate\" => exchanges::gate::fetch_open_interest(market_type, symbol.unwrap()),\n        \"huobi\" => exchanges::huobi::fetch_open_interest(market_type, symbol),\n        \"kucoin\" => exchanges::kucoin::fetch_open_interest(market_type),\n        \"okx\" => exchanges::okx::OkxRestClient::fetch_open_interest(market_type, symbol),\n        \"zbg\" => exchanges::zbg::fetch_open_interest(market_type, symbol.unwrap()),\n        _ => panic!(\"{exchange} does NOT have open interest RESTful API\"),\n    };\n    match ret {\n        Ok(s) => Ok(s.trim().to_string()),\n        Err(_) => ret,\n    }\n}\n\npub fn fetch_long_short_ratio(\n    exchange: &str,\n    market_type: MarketType,\n    symbol: &str,\n) -> Result<String> {\n    let ret = match exchange {\n        \"bybit\" => exchanges::bybit::BybitRestClient::fetch_long_short_ratio(symbol),\n        _ => panic!(\"{exchange} {market_type} does NOT provide level3 orderbook data\"),\n    };\n    match ret {\n        Ok(s) => Ok(s.trim().to_string()),\n        Err(_) => ret,\n    }\n}\n\n/// Fetch level2 orderbook snapshot.\n///\n/// `retry` None means no retry; Some(0) means retry unlimited times; Some(n)\n/// means retry n times.\npub fn fetch_l2_snapshot(\n    exchange: &str,\n    market_type: MarketType,\n    symbol: &str,\n    retry: Option<u64>,\n) -> Result<String> {\n    retriable(exchange, market_type, symbol, fetch_l2_snapshot_internal, retry)\n}\n\n/// Fetch level3 orderbook snapshot.\n///\n/// `retry` None means no retry; Some(0) means retry unlimited times; Some(n)\n/// means retry n times.\npub fn fetch_l3_snapshot(\n    exchange: &str,\n    market_type: MarketType,\n    symbol: &str,\n    retry: Option<u64>,\n) -> Result<String> {\n    retriable(exchange, market_type, symbol, fetch_l3_snapshot_internal, retry)\n}\n\n// `retry` None means no retry; Some(0) means retry unlimited times; Some(n)\n// means retry n times.\nfn retriable(\n    exchange: &str,\n    market_type: MarketType,\n    symbol: &str,\n    crawl_func: fn(&str, MarketType, &str) -> Result<String>,\n    retry: Option<u64>,\n) -> Result<String> {\n    let retry_count = {\n        let count = retry.unwrap_or(1);\n        if count == 0 { u64::MAX } else { count }\n    };\n    if retry_count == 1 {\n        return crawl_func(exchange, market_type, symbol);\n    }\n    let mut backoff_factor = 0;\n    let cooldown_time = Duration::from_secs(2);\n    for _ in 0..retry_count {\n        let resp = crawl_func(exchange, market_type, symbol);\n        match resp {\n            Ok(msg) => return Ok(msg),\n            Err(err) => {\n                let current_timestamp =\n                    SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_millis()\n                        as u64;\n                warn!(\n                    \"{} {} {} {} {}, error: {}, back off for {} milliseconds\",\n                    current_timestamp,\n                    backoff_factor,\n                    exchange,\n                    market_type,\n                    symbol,\n                    err,\n                    (backoff_factor * cooldown_time).as_millis()\n                );\n                std::thread::sleep(backoff_factor * cooldown_time);\n                if err.0.contains(\"429\") {\n                    backoff_factor += 1;\n                } else {\n                    // Handle 403, 418, etc.\n                    backoff_factor *= 2;\n                }\n            }\n        }\n    }\n    Err(Error(format!(\n        \"Failed {exchange} {market_type} {symbol} after retrying {retry_count} times\"\n    )))\n}\n"
  },
  {
    "path": "crypto-rest-client/tests/binance_inverse.rs",
    "content": "#[cfg(test)]\nmod inverse_swap {\n    use crypto_market_type::MarketType;\n    use crypto_rest_client::{fetch_l2_snapshot, fetch_open_interest, BinanceInverseRestClient};\n\n    #[test]\n    fn test_agg_trades() {\n        let text =\n            BinanceInverseRestClient::fetch_agg_trades(\"BTCUSD_PERP\", None, None, None).unwrap();\n        assert!(text.starts_with(\"[{\"));\n    }\n\n    #[test]\n    fn test_l2_snapshot() {\n        let text =\n            fetch_l2_snapshot(\"binance\", MarketType::InverseSwap, \"BTCUSD_PERP\", Some(3)).unwrap();\n        assert!(text.starts_with('{'));\n    }\n\n    #[test]\n    fn test_open_interest() {\n        let text =\n            fetch_open_interest(\"binance\", MarketType::InverseSwap, Some(\"BTCUSD_PERP\")).unwrap();\n        assert!(text.starts_with('{'));\n    }\n}\n\n#[cfg(test)]\nmod inverse_future {\n    use crypto_market_type::MarketType;\n    use crypto_rest_client::{fetch_l2_snapshot, fetch_open_interest, BinanceInverseRestClient};\n\n    #[test]\n    fn test_agg_trades() {\n        let text =\n            BinanceInverseRestClient::fetch_agg_trades(\"BTCUSD_221230\", None, None, None).unwrap();\n        assert!(text.starts_with(\"[{\"));\n    }\n\n    #[test]\n    fn test_l2_snapshot() {\n        let text =\n            fetch_l2_snapshot(\"binance\", MarketType::InverseFuture, \"BTCUSD_221230\", Some(3))\n                .unwrap();\n        assert!(text.starts_with('{'));\n    }\n\n    #[test]\n    fn test_open_interest() {\n        let text = fetch_open_interest(\"binance\", MarketType::InverseFuture, Some(\"BTCUSD_221230\"))\n            .unwrap();\n        assert!(text.starts_with('{'));\n    }\n}\n"
  },
  {
    "path": "crypto-rest-client/tests/binance_linear.rs",
    "content": "#[cfg(test)]\nmod linear_swap {\n    use crypto_market_type::MarketType;\n    use crypto_rest_client::{fetch_l2_snapshot, fetch_open_interest, BinanceLinearRestClient};\n\n    #[test]\n    fn test_agg_trades() {\n        let text = BinanceLinearRestClient::fetch_agg_trades(\"BTCUSDT\", None, None, None).unwrap();\n        assert!(text.starts_with(\"[{\"));\n    }\n\n    #[test]\n    fn test_l2_snapshot() {\n        let text =\n            fetch_l2_snapshot(\"binance\", MarketType::LinearSwap, \"BTCUSDT\", Some(3)).unwrap();\n        assert!(text.starts_with('{'));\n    }\n\n    #[test]\n    fn test_open_interest() {\n        let text = fetch_open_interest(\"binance\", MarketType::LinearSwap, Some(\"BTCUSDT\")).unwrap();\n        assert!(text.starts_with('{'));\n    }\n}\n\n#[cfg(test)]\nmod linear_future {\n    use crypto_market_type::MarketType;\n    use crypto_rest_client::{fetch_l2_snapshot, fetch_open_interest, BinanceLinearRestClient};\n\n    #[test]\n    fn test_agg_trades() {\n        let text =\n            BinanceLinearRestClient::fetch_agg_trades(\"BTCUSDT_221230\", None, None, None).unwrap();\n        assert!(text.starts_with(\"[{\"));\n    }\n\n    #[test]\n    fn test_l2_snapshot() {\n        let text =\n            fetch_l2_snapshot(\"binance\", MarketType::LinearFuture, \"BTCUSDT_221230\", Some(3))\n                .unwrap();\n        assert!(text.starts_with('{'));\n    }\n\n    #[test]\n    fn test_open_interest() {\n        let text = fetch_open_interest(\"binance\", MarketType::LinearFuture, Some(\"BTCUSDT_221230\"))\n            .unwrap();\n        assert!(text.starts_with('{'));\n    }\n}\n"
  },
  {
    "path": "crypto-rest-client/tests/binance_option.rs",
    "content": "use crypto_market_type::MarketType;\nuse crypto_rest_client::{fetch_l2_snapshot, BinanceOptionRestClient};\n\n#[ignore]\n#[test]\nfn test_agg_trades() {\n    let text = BinanceOptionRestClient::fetch_trades(\"BTC-220610-30000-C\", None).unwrap();\n    assert!(text.starts_with('{'));\n}\n\n#[ignore]\n#[test]\nfn test_l2_snapshot() {\n    let text =\n        fetch_l2_snapshot(\"binance\", MarketType::EuropeanOption, \"BTC-220610-30000-C\", Some(3))\n            .unwrap();\n    assert!(text.starts_with('{'));\n}\n"
  },
  {
    "path": "crypto-rest-client/tests/binance_spot.rs",
    "content": "use crypto_market_type::MarketType;\nuse crypto_rest_client::{fetch_l2_snapshot, BinanceSpotRestClient};\n\n#[test]\nfn test_agg_trades() {\n    let text = BinanceSpotRestClient::fetch_agg_trades(\"BTCUSDT\", None, None, None).unwrap();\n    assert!(text.starts_with(\"[{\"));\n}\n\n#[test]\nfn test_l2_snapshot() {\n    let text = fetch_l2_snapshot(\"binance\", MarketType::Spot, \"BTCUSDT\", Some(3)).unwrap();\n    assert!(text.starts_with('{'));\n}\n"
  },
  {
    "path": "crypto-rest-client/tests/bitfinex.rs",
    "content": "use crypto_market_type::MarketType;\nuse crypto_rest_client::{fetch_l2_snapshot, fetch_l3_snapshot, BitfinexRestClient};\n\n#[test]\nfn test_trades() {\n    let text = BitfinexRestClient::fetch_trades(\"tBTCUSD\", None, None, None, None).unwrap();\n    assert!(text.starts_with(\"[[\"));\n}\n\n#[test]\nfn test_l2_snapshot() {\n    let text = fetch_l2_snapshot(\"bitfinex\", MarketType::Spot, \"tBTCUSD\", Some(3)).unwrap();\n    assert!(text.starts_with(\"[[\"));\n}\n\n#[test]\nfn test_l3_snapshot() {\n    let text = fetch_l3_snapshot(\"bitfinex\", MarketType::Spot, \"tBTCUSD\", Some(3)).unwrap();\n    assert!(text.starts_with(\"[[\"));\n}\n"
  },
  {
    "path": "crypto-rest-client/tests/bitget_spot.rs",
    "content": "use crypto_market_type::MarketType;\nuse crypto_rest_client::fetch_l2_snapshot;\nuse serde_json::Value;\nuse std::collections::HashMap;\n\n#[test]\nfn test_l2_snapshot() {\n    let text = fetch_l2_snapshot(\"bitget\", MarketType::Spot, \"BTCUSDT_SPBL\", Some(3)).unwrap();\n    let obj = serde_json::from_str::<HashMap<String, Value>>(&text).unwrap();\n\n    assert_eq!(obj.get(\"msg\").unwrap().as_str().unwrap(), \"success\");\n\n    let data = obj.get(\"data\").unwrap().as_object().unwrap();\n    assert!(!data.get(\"asks\").unwrap().as_array().unwrap().is_empty());\n    assert!(!data.get(\"bids\").unwrap().as_array().unwrap().is_empty());\n}\n"
  },
  {
    "path": "crypto-rest-client/tests/bitget_swap.rs",
    "content": "use crypto_market_type::MarketType;\nuse crypto_rest_client::{fetch_l2_snapshot, fetch_open_interest};\nuse serde_json::Value;\nuse std::collections::HashMap;\nuse test_case::test_case;\n\n#[test_case(MarketType::InverseFuture, \"BTCUSD_DMCBL_221230\")]\n#[test_case(MarketType::InverseSwap, \"BTCUSD_DMCBL\")]\n#[test_case(MarketType::LinearSwap, \"BTCUSDT_UMCBL\")]\nfn test_l2_snapshot(market_type: MarketType, symbol: &str) {\n    let text = fetch_l2_snapshot(\"bitget\", market_type, symbol, Some(3)).unwrap();\n    let obj = serde_json::from_str::<HashMap<String, Value>>(&text).unwrap();\n\n    let data = obj.get(\"data\").unwrap().as_object().unwrap();\n    assert!(!data.get(\"asks\").unwrap().as_array().unwrap().is_empty());\n    assert!(!data.get(\"bids\").unwrap().as_array().unwrap().is_empty());\n}\n\n#[test_case(MarketType::InverseFuture, \"BTCUSD_DMCBL_221230\")]\n#[test_case(MarketType::InverseSwap, \"BTCUSD_DMCBL\")]\n#[test_case(MarketType::LinearSwap, \"BTCUSDT_UMCBL\")]\nfn test_open_interest(market_type: MarketType, symbol: &str) {\n    let text = fetch_open_interest(\"bitget\", market_type, Some(symbol)).unwrap();\n    let obj = serde_json::from_str::<HashMap<String, Value>>(&text).unwrap();\n\n    let data = obj.get(\"data\").unwrap().as_object().unwrap();\n    assert!(data.contains_key(\"amount\"));\n}\n"
  },
  {
    "path": "crypto-rest-client/tests/bithumb.rs",
    "content": "use std::collections::HashMap;\n\nuse crypto_market_type::MarketType;\nuse crypto_rest_client::{fetch_l2_snapshot, BithumbRestClient};\nuse serde_json::Value;\n\n#[test]\nfn test_trades() {\n    let text = BithumbRestClient::fetch_trades(\"BTC-USDT\").unwrap();\n    let obj = serde_json::from_str::<HashMap<String, Value>>(&text).unwrap();\n    assert_eq!(obj.get(\"code\").unwrap().as_str().unwrap(), \"0\");\n\n    let data = obj.get(\"data\").unwrap().as_array().unwrap();\n    assert!(!data.is_empty());\n}\n\n#[test]\nfn test_l2_snapshot() {\n    let text = fetch_l2_snapshot(\"bithumb\", MarketType::Spot, \"BTC-USDT\", Some(3)).unwrap();\n    let obj = serde_json::from_str::<HashMap<String, Value>>(&text).unwrap();\n    assert_eq!(obj.get(\"code\").unwrap().as_str().unwrap(), \"0\");\n\n    let data = obj.get(\"data\").unwrap().as_object().unwrap();\n    let buy = data.get(\"b\").unwrap().as_array().unwrap();\n    let sell = data.get(\"s\").unwrap().as_array().unwrap();\n    assert!(!buy.is_empty());\n    assert!(!sell.is_empty());\n}\n"
  },
  {
    "path": "crypto-rest-client/tests/bitmex.rs",
    "content": "use crypto_market_type::MarketType;\nuse crypto_rest_client::{fetch_l2_snapshot, BitmexRestClient};\n\n#[test]\nfn test_trades() {\n    let text = BitmexRestClient::fetch_trades(\"XBTUSD\", None).unwrap();\n    assert!(text.starts_with(\"[{\"));\n}\n\n#[test]\nfn test_l2_snapshot() {\n    let text = fetch_l2_snapshot(\"bitmex\", MarketType::InverseSwap, \"XBTUSD\", Some(3)).unwrap();\n    assert!(text.starts_with(\"[{\"));\n}\n"
  },
  {
    "path": "crypto-rest-client/tests/bitstamp.rs",
    "content": "use crypto_market_type::MarketType;\nuse crypto_rest_client::{fetch_l2_snapshot, fetch_l3_snapshot, BitstampRestClient};\n\n#[test]\nfn test_trades() {\n    let text = BitstampRestClient::fetch_trades(\"btcusd\", Some(\"minute\".to_string())).unwrap();\n    assert!(text.starts_with(\"[{\"));\n}\n\n#[test]\nfn test_l2_snapshot() {\n    let text = fetch_l2_snapshot(\"bitstamp\", MarketType::Spot, \"btcusd\", Some(3)).unwrap();\n    assert!(text.starts_with('{'));\n}\n\n#[test]\nfn test_l3_snapshot() {\n    let text = fetch_l3_snapshot(\"bitstamp\", MarketType::Spot, \"btcusd\", Some(3)).unwrap();\n    assert!(text.starts_with('{'));\n}\n"
  },
  {
    "path": "crypto-rest-client/tests/bitz_spot.rs",
    "content": "use std::collections::HashMap;\n\nuse crypto_market_type::MarketType;\nuse crypto_rest_client::fetch_l2_snapshot;\nuse serde_json::Value;\n\n#[test]\n#[ignore = \"bitz.com has shutdown since October 2021\"]\nfn test_l2_snapshot() {\n    let text = fetch_l2_snapshot(\"bitz\", MarketType::Spot, \"btc_usdt\", Some(3)).unwrap();\n    let obj = serde_json::from_str::<HashMap<String, Value>>(&text).unwrap();\n\n    assert_eq!(obj.get(\"status\").unwrap().as_i64().unwrap(), 200);\n\n    let data = obj.get(\"data\").unwrap().as_object().unwrap();\n    assert!(!data.get(\"asks\").unwrap().as_array().unwrap().is_empty());\n    assert!(!data.get(\"bids\").unwrap().as_array().unwrap().is_empty());\n}\n"
  },
  {
    "path": "crypto-rest-client/tests/bitz_swap.rs",
    "content": "use std::collections::HashMap;\n\nuse crypto_market_type::MarketType;\nuse crypto_rest_client::{fetch_l2_snapshot, fetch_open_interest};\nuse serde_json::Value;\nuse test_case::test_case;\n\n#[test_case(MarketType::InverseSwap, \"BTC_USD\"; \"inconclusive 1\")]\n#[test_case(MarketType::LinearSwap, \"BTC_USDT\"; \"inconclusive 2\")]\nfn test_l2_snapshot(market_type: MarketType, symbol: &str) {\n    let text = fetch_l2_snapshot(\"bitz\", market_type, symbol, Some(3)).unwrap();\n    let obj = serde_json::from_str::<HashMap<String, Value>>(&text).unwrap();\n\n    assert_eq!(obj.get(\"status\").unwrap().as_i64().unwrap(), 200);\n\n    let data = obj.get(\"data\").unwrap().as_object().unwrap();\n    assert!(!data.get(\"asks\").unwrap().as_array().unwrap().is_empty());\n    assert!(!data.get(\"bids\").unwrap().as_array().unwrap().is_empty());\n}\n\n#[test_case(MarketType::InverseSwap; \"inconclusive 1\")]\n#[test_case(MarketType::LinearSwap; \"inconclusive 2\")]\nfn test_open_interest(market_type: MarketType) {\n    let text = fetch_open_interest(\"bitz\", market_type, None).unwrap();\n    let obj = serde_json::from_str::<HashMap<String, Value>>(&text).unwrap();\n    let arr = obj.get(\"data\").unwrap().as_array().unwrap();\n    assert!(!arr.is_empty());\n}\n"
  },
  {
    "path": "crypto-rest-client/tests/bybit.rs",
    "content": "use std::collections::HashMap;\n\nuse crypto_market_type::MarketType;\nuse crypto_rest_client::{fetch_l2_snapshot, fetch_long_short_ratio, fetch_open_interest};\nuse serde_json::Value;\nuse test_case::test_case;\n\n#[test_case(MarketType::InverseFuture, \"BTCUSDZ22\")]\n#[test_case(MarketType::InverseSwap, \"BTCUSD\")]\n#[test_case(MarketType::LinearSwap, \"BTCUSDT\")]\nfn test_l2_snapshot(market_type: MarketType, symbol: &str) {\n    let text = fetch_l2_snapshot(\"bybit\", market_type, symbol, Some(3)).unwrap();\n\n    let obj = serde_json::from_str::<HashMap<String, Value>>(&text).unwrap();\n    let result = obj.get(\"result\").unwrap();\n\n    assert!(result.is_array());\n    assert_eq!(result.as_array().unwrap().len(), 50);\n}\n\n#[test_case(MarketType::InverseFuture, \"BTCUSDZ22\")]\n#[test_case(MarketType::InverseSwap, \"BTCUSD\")]\n#[test_case(MarketType::LinearSwap, \"BTCUSDT\")]\nfn test_open_interest(market_type: MarketType, symbol: &str) {\n    let text = fetch_open_interest(\"bybit\", market_type, Some(symbol)).unwrap();\n\n    let obj = serde_json::from_str::<HashMap<String, Value>>(&text).unwrap();\n    let result = obj.get(\"result\").unwrap().as_array().unwrap();\n\n    assert!(!result.is_empty());\n}\n\n#[test_case(MarketType::InverseFuture, \"BTCUSDZ22\")]\n#[test_case(MarketType::InverseSwap, \"BTCUSD\")]\n#[test_case(MarketType::LinearSwap, \"BTCUSDT\")]\nfn test_long_short_ratio(market_type: MarketType, symbol: &str) {\n    let text = fetch_long_short_ratio(\"bybit\", market_type, symbol).unwrap();\n\n    let obj = serde_json::from_str::<HashMap<String, Value>>(&text).unwrap();\n    let result = obj.get(\"result\").unwrap().as_array().unwrap();\n\n    assert!(!result.is_empty());\n}\n"
  },
  {
    "path": "crypto-rest-client/tests/coinbase_pro.rs",
    "content": "use crypto_market_type::MarketType;\nuse crypto_rest_client::{fetch_l2_snapshot, fetch_l3_snapshot, CoinbaseProRestClient};\n\n#[test]\nfn test_trades() {\n    let text = CoinbaseProRestClient::fetch_trades(\"BTC-USD\").unwrap();\n    assert!(text.starts_with(\"[{\"));\n}\n\n#[test]\nfn test_l2_snapshot() {\n    let text = fetch_l2_snapshot(\"coinbase_pro\", MarketType::Spot, \"BTC-USD\", Some(3)).unwrap();\n    assert!(text.starts_with('{'));\n}\n\n#[test]\nfn test_l3_snapshot() {\n    let text = fetch_l3_snapshot(\"coinbase_pro\", MarketType::Spot, \"BTC-USD\", Some(3)).unwrap();\n    assert!(text.starts_with('{'));\n}\n"
  },
  {
    "path": "crypto-rest-client/tests/deribit.rs",
    "content": "use crypto_market_type::MarketType;\nuse crypto_rest_client::{fetch_l2_snapshot, fetch_open_interest, DeribitRestClient};\nuse serde_json::Value;\nuse std::collections::HashMap;\nuse test_case::test_case;\n\n#[test_case(\"BTC-PERPETUAL\")]\n#[test_case(\"BTC-30DEC22\")]\n#[test_case(\"BTC-29JUL22-20000-C\")]\nfn test_trades(symbol: &str) {\n    let text = DeribitRestClient::fetch_trades(symbol).unwrap();\n\n    let obj = serde_json::from_str::<HashMap<String, Value>>(&text).unwrap();\n    let result = obj.get(\"result\").unwrap().as_object().unwrap();\n\n    assert!(result.get(\"trades\").unwrap().is_array());\n}\n\n#[test_case(MarketType::InverseSwap, \"BTC-PERPETUAL\")]\n#[test_case(MarketType::InverseFuture, \"BTC-30DEC22\")]\n#[test_case(MarketType::EuropeanOption, \"BTC-29JUL22-20000-C\")]\nfn test_l2_snapshot(market_type: MarketType, symbol: &str) {\n    let text = fetch_l2_snapshot(\"deribit\", market_type, symbol, Some(3)).unwrap();\n\n    let obj = serde_json::from_str::<HashMap<String, Value>>(&text).unwrap();\n    let result = obj.get(\"result\").unwrap().as_object().unwrap();\n\n    assert!(result.get(\"asks\").unwrap().is_array());\n    assert!(result.get(\"bids\").unwrap().is_array())\n}\n\n#[test_case(MarketType::InverseFuture)]\n#[test_case(MarketType::InverseSwap)]\n#[test_case(MarketType::EuropeanOption)]\nfn test_open_interest(market_type: MarketType) {\n    let text = fetch_open_interest(\"deribit\", market_type, None).unwrap();\n    for line in text.lines() {\n        let obj = serde_json::from_str::<HashMap<String, Value>>(line).unwrap();\n        let arr = obj.get(\"result\").unwrap().as_array().unwrap();\n        assert!(!arr.is_empty());\n    }\n}\n"
  },
  {
    "path": "crypto-rest-client/tests/dydx.rs",
    "content": "use crypto_market_type::MarketType;\nuse crypto_rest_client::{fetch_l2_snapshot, fetch_open_interest};\nuse serde_json::Value;\nuse std::collections::HashMap;\nuse test_case::test_case;\n\n#[test_case(MarketType::LinearSwap, \"BTC-USD\")]\nfn test_l2_snapshot(market_type: MarketType, symbol: &str) {\n    let text = fetch_l2_snapshot(\"dydx\", market_type, symbol, Some(3)).unwrap();\n    let obj = serde_json::from_str::<HashMap<String, Value>>(&text).unwrap();\n\n    let asks = obj.get(\"asks\").unwrap().as_array().unwrap();\n    let bids = obj.get(\"bids\").unwrap().as_array().unwrap();\n    assert!(!asks.is_empty());\n    assert!(!bids.is_empty());\n}\n\n#[test_case(MarketType::LinearSwap)]\nfn test_open_interest(market_type: MarketType) {\n    let text = fetch_open_interest(\"dydx\", market_type, None).unwrap();\n    println!(\"{text}\");\n    let obj = serde_json::from_str::<HashMap<String, Value>>(&text).unwrap();\n    assert!(obj.contains_key(\"markets\"));\n}\n"
  },
  {
    "path": "crypto-rest-client/tests/ftx.rs",
    "content": "use std::collections::HashMap;\n\nuse crypto_market_type::MarketType;\nuse crypto_rest_client::{fetch_l2_snapshot, fetch_open_interest};\nuse serde_json::Value;\nuse test_case::test_case;\n\n#[test_case(MarketType::Spot, \"BTC/USD\")]\n#[test_case(MarketType::LinearSwap, \"BTC-PERP\")]\n#[test_case(MarketType::LinearFuture, \"BTC-1230\")]\n#[test_case(MarketType::Move, \"BTC-MOVE-2022Q4\")]\n#[test_case(MarketType::BVOL, \"BVOL/USD\")]\nfn test_l2_snapshot(market_type: MarketType, symbol: &str) {\n    let text = fetch_l2_snapshot(\"ftx\", market_type, symbol, Some(3)).unwrap();\n\n    let obj = serde_json::from_str::<HashMap<String, Value>>(&text).unwrap();\n    let result = obj.get(\"result\").unwrap().as_object().unwrap();\n\n    assert!(result.get(\"asks\").unwrap().is_array());\n    assert!(result.get(\"bids\").unwrap().is_array())\n}\n\n#[test]\nfn test_open_interest() {\n    let text = fetch_open_interest(\"ftx\", MarketType::Unknown, None).unwrap();\n    let obj = serde_json::from_str::<HashMap<String, Value>>(&text).unwrap();\n    let result = obj.get(\"result\").unwrap().as_array().unwrap();\n    assert!(!result.is_empty())\n}\n"
  },
  {
    "path": "crypto-rest-client/tests/gate.rs",
    "content": "use crypto_market_type::MarketType;\nuse crypto_rest_client::{fetch_l2_snapshot, fetch_open_interest};\nuse serde_json::Value;\nuse std::collections::HashMap;\nuse test_case::test_case;\n\n#[test_case(MarketType::Spot, \"BTC_USDT\")]\n#[test_case(MarketType::InverseSwap, \"BTC_USD\")]\n#[test_case(MarketType::LinearSwap, \"BTC_USDT\")]\n#[test_case(MarketType::InverseFuture, \"BTC_USD_20221230\")]\n#[test_case(MarketType::LinearFuture, \"BTC_USDT_20221230\")]\nfn test_l2_snapshot(market_type: MarketType, symbol: &str) {\n    let text = fetch_l2_snapshot(\"gate\", market_type, symbol, Some(3)).unwrap();\n    let obj = serde_json::from_str::<HashMap<String, Value>>(&text).unwrap();\n\n    let asks = obj.get(\"asks\").unwrap().as_array().unwrap();\n    assert!(!asks.is_empty());\n\n    let bids = obj.get(\"bids\").unwrap().as_array().unwrap();\n    assert!(!bids.is_empty());\n}\n\n#[test_case(MarketType::InverseSwap, \"BTC_USD\")]\n#[test_case(MarketType::LinearSwap, \"BTC_USDT\")]\nfn test_open_interest(market_type: MarketType, symbol: &str) {\n    let text = fetch_open_interest(\"gate\", market_type, Some(symbol)).unwrap();\n    let arr = serde_json::from_str::<Vec<Value>>(&text).unwrap();\n    assert!(!arr.is_empty());\n}\n"
  },
  {
    "path": "crypto-rest-client/tests/huobi.rs",
    "content": "use crypto_market_type::MarketType;\nuse crypto_rest_client::{fetch_l2_snapshot, fetch_open_interest};\nuse serde_json::Value;\nuse std::collections::HashMap;\nuse test_case::test_case;\n\n#[test_case(MarketType::Spot, \"btcusdt\")]\n#[test_case(MarketType::InverseFuture, \"BTC_CQ\")]\n#[test_case(MarketType::InverseSwap, \"BTC-USD\")]\n#[test_case(MarketType::LinearSwap, \"BTC-USDT\")]\n#[test_case(MarketType::EuropeanOption, \"BTC-USDT-210625-P-27000\"; \"inconclusive\")]\nfn test_l2_snapshot(market_type: MarketType, symbol: &str) {\n    let text = fetch_l2_snapshot(\"huobi\", market_type, symbol, Some(3)).unwrap();\n    assert!(text.starts_with('{'));\n\n    let obj = serde_json::from_str::<HashMap<String, Value>>(&text).unwrap();\n    assert_eq!(\"ok\", obj.get(\"status\").unwrap().as_str().unwrap());\n\n    let bids =\n        obj.get(\"tick\").unwrap().as_object().unwrap().get(\"bids\").unwrap().as_array().unwrap();\n    assert!(!bids.is_empty());\n\n    let asks =\n        obj.get(\"tick\").unwrap().as_object().unwrap().get(\"asks\").unwrap().as_array().unwrap();\n    assert!(!asks.is_empty());\n}\n\n#[test_case(MarketType::InverseFuture)]\n#[test_case(MarketType::InverseSwap)]\n#[test_case(MarketType::LinearSwap)]\nfn test_open_interest(market_type: MarketType) {\n    let text = fetch_open_interest(\"huobi\", market_type, None).unwrap();\n    let obj = serde_json::from_str::<HashMap<String, Value>>(&text).unwrap();\n    let arr = obj.get(\"data\").unwrap().as_array().unwrap();\n    assert!(!arr.is_empty());\n}\n\n#[cfg(test)]\nmod huobi_spot {\n    use crypto_rest_client::HuobiSpotRestClient;\n\n    #[test]\n    fn test_trades() {\n        let text = HuobiSpotRestClient::fetch_trades(\"btcusdt\").unwrap();\n        assert!(text.starts_with('{'));\n    }\n}\n\n#[cfg(test)]\nmod huobi_future {\n    use crypto_rest_client::HuobiFutureRestClient;\n\n    #[test]\n    fn test_trades() {\n        let text = HuobiFutureRestClient::fetch_trades(\"BTC_CQ\").unwrap();\n        assert!(text.starts_with('{'));\n    }\n}\n\n#[cfg(test)]\nmod huobi_linear_swap {\n    use crypto_rest_client::HuobiLinearSwapRestClient;\n\n    #[test]\n    fn test_trades() {\n        let text = HuobiLinearSwapRestClient::fetch_trades(\"BTC-USDT\").unwrap();\n        assert!(text.starts_with('{'));\n    }\n}\n\n#[cfg(test)]\nmod huobi_inverse_swap {\n    use crypto_rest_client::HuobiInverseSwapRestClient;\n\n    #[test]\n    fn test_trades() {\n        let text = HuobiInverseSwapRestClient::fetch_trades(\"BTC-USD\").unwrap();\n        assert!(text.starts_with('{'));\n    }\n}\n\n#[cfg(test)]\nmod huobi_option {\n    use crypto_rest_client::HuobiOptionRestClient;\n\n    #[test]\n    #[ignore]\n    fn test_trades() {\n        let text = HuobiOptionRestClient::fetch_trades(\"BTC-USDT-210625-P-27000\").unwrap();\n        assert!(text.starts_with('{'));\n    }\n}\n"
  },
  {
    "path": "crypto-rest-client/tests/kraken.rs",
    "content": "use std::collections::HashMap;\n\nuse crypto_market_type::MarketType;\nuse crypto_rest_client::{fetch_l2_snapshot, KrakenSpotRestClient};\nuse serde_json::Value;\nuse test_case::test_case;\n\n#[test]\nfn test_trades() {\n    let text = KrakenSpotRestClient::fetch_trades(\"XBTUSD\", None).unwrap();\n    let obj = serde_json::from_str::<HashMap<String, Value>>(&text).unwrap();\n    assert!(obj.get(\"error\").unwrap().as_array().unwrap().is_empty());\n\n    let text = KrakenSpotRestClient::fetch_trades(\"XBT/USD\", None).unwrap();\n    let obj = serde_json::from_str::<HashMap<String, Value>>(&text).unwrap();\n    assert!(obj.get(\"error\").unwrap().as_array().unwrap().is_empty());\n}\n\n#[test]\nfn test_restful_accepts_two_symbols() {\n    let text = fetch_l2_snapshot(\"kraken\", MarketType::Spot, \"XBTUSD\", Some(3)).unwrap();\n    let obj = serde_json::from_str::<HashMap<String, Value>>(&text).unwrap();\n    assert!(obj.get(\"error\").unwrap().as_array().unwrap().is_empty());\n\n    let text = fetch_l2_snapshot(\"kraken\", MarketType::Spot, \"XBT/USD\", Some(3)).unwrap();\n    let obj = serde_json::from_str::<HashMap<String, Value>>(&text).unwrap();\n    assert!(obj.get(\"error\").unwrap().as_array().unwrap().is_empty());\n}\n\n#[test_case(MarketType::Spot, \"XBTUSD\")]\n#[test_case(MarketType::Spot, \"XBT/USD\")]\n#[test_case(MarketType::InverseFuture, \"FI_XBTUSD_221230\")]\n#[test_case(MarketType::InverseSwap, \"PI_XBTUSD\")]\nfn test_l2_snapshot(market_type: MarketType, symbol: &str) {\n    let text = fetch_l2_snapshot(\"kraken\", market_type, symbol, None).unwrap();\n    let obj = serde_json::from_str::<HashMap<String, Value>>(&text).unwrap();\n    if market_type == MarketType::Spot {\n        assert!(obj.get(\"error\").unwrap().as_array().unwrap().is_empty());\n    } else {\n        assert_eq!(obj.get(\"result\").unwrap().as_str().unwrap(), \"success\");\n    }\n}\n"
  },
  {
    "path": "crypto-rest-client/tests/kucoin.rs",
    "content": "use crypto_market_type::MarketType;\nuse crypto_rest_client::{fetch_l2_snapshot, fetch_l3_snapshot, fetch_open_interest};\nuse serde_json::Value;\nuse std::collections::HashMap;\nuse test_case::test_case;\n\n#[test_case(MarketType::Spot, \"BTC-USDT\")]\n#[test_case(MarketType::InverseFuture, \"XBTMZ22\")]\n#[test_case(MarketType::InverseSwap, \"XBTUSDM\")]\n#[test_case(MarketType::LinearSwap, \"XBTUSDTM\")]\nfn test_l2_snapshot(market_type: MarketType, symbol: &str) {\n    let text = fetch_l2_snapshot(\"kucoin\", market_type, symbol, Some(3)).unwrap();\n\n    let obj = serde_json::from_str::<HashMap<String, Value>>(&text).unwrap();\n    assert_eq!(\"200000\", obj.get(\"code\").unwrap().as_str().unwrap());\n\n    let data = obj.get(\"data\").unwrap().as_object().unwrap();\n\n    let asks = data.get(\"asks\").unwrap().as_array().unwrap();\n    assert!(!asks.is_empty());\n\n    let bids = data.get(\"bids\").unwrap().as_array().unwrap();\n    assert!(!bids.is_empty());\n}\n\n#[test_case(MarketType::Spot, \"BTC-USDT\"; \"inconclusive\")] // TODO: kucoin deprecated level2 and level3 snapshot APIs\n#[test_case(MarketType::InverseFuture, \"XBTMZ22\")]\n#[test_case(MarketType::InverseSwap, \"XBTUSDM\")]\n#[test_case(MarketType::LinearSwap, \"XBTUSDTM\")]\nfn test_l3_snapshot(market_type: MarketType, symbol: &str) {\n    let text = fetch_l3_snapshot(\"kucoin\", market_type, symbol, Some(3)).unwrap();\n\n    let obj = serde_json::from_str::<HashMap<String, Value>>(&text).unwrap();\n    assert_eq!(\"200000\", obj.get(\"code\").unwrap().as_str().unwrap());\n\n    let data = obj.get(\"data\").unwrap().as_object().unwrap();\n\n    let asks = data.get(\"asks\").unwrap().as_array().unwrap();\n    assert!(!asks.is_empty());\n\n    let bids = data.get(\"bids\").unwrap().as_array().unwrap();\n    assert!(!bids.is_empty());\n}\n\n#[test_case(MarketType::Unknown)]\n#[test_case(MarketType::LinearSwap)]\nfn test_open_interest(market_type: MarketType) {\n    let text = fetch_open_interest(\"kucoin\", market_type, None).unwrap();\n    let obj = serde_json::from_str::<HashMap<String, Value>>(&text).unwrap();\n    let arr = obj.get(\"data\").unwrap().as_array().unwrap();\n    assert!(!arr.is_empty());\n}\n"
  },
  {
    "path": "crypto-rest-client/tests/mexc.rs",
    "content": "#[cfg(test)]\nmod mexc_spot {\n    use crypto_rest_client::MexcSpotRestClient;\n\n    #[test]\n    fn test_trades() {\n        let text = MexcSpotRestClient::fetch_trades(\"BTC_USDT\").unwrap();\n        assert!(text.starts_with('{'));\n    }\n\n    #[test]\n    fn test_l2_snapshot() {\n        let text = MexcSpotRestClient::fetch_l2_snapshot(\"BTC_USDT\").unwrap();\n        assert!(text.starts_with('{'));\n    }\n}\n\n#[cfg(test)]\nmod mexc_swap {\n    use crypto_market_type::MarketType;\n    use crypto_rest_client::{fetch_l2_snapshot, MexcSwapRestClient};\n\n    #[test]\n    fn test_trades() {\n        let text = MexcSwapRestClient::fetch_trades(\"BTC_USDT\").unwrap();\n        assert!(text.starts_with('{'));\n    }\n\n    #[test]\n    fn test_l2_snapshot() {\n        let text = fetch_l2_snapshot(\"mexc\", MarketType::LinearSwap, \"BTC_USDT\", Some(3)).unwrap();\n        assert!(text.starts_with('{'));\n    }\n}\n"
  },
  {
    "path": "crypto-rest-client/tests/okx.rs",
    "content": "use crypto_market_type::MarketType;\nuse crypto_rest_client::{fetch_l2_snapshot, fetch_open_interest};\nuse serde_json::Value;\nuse std::collections::HashMap;\nuse test_case::test_case;\n\n#[test_case(MarketType::Spot, \"BTC-USDT\")]\n#[test_case(MarketType::InverseFuture, \"BTC-USD-221230\")]\n#[test_case(MarketType::LinearFuture, \"BTC-USDT-221230\")]\n#[test_case(MarketType::InverseSwap, \"BTC-USD-SWAP\")]\n#[test_case(MarketType::LinearSwap, \"BTC-USDT-SWAP\")]\n#[test_case(MarketType::EuropeanOption, \"BTC-USD-221230-10000-P\")]\nfn test_l2_snapshot(market_type: MarketType, symbol: &str) {\n    let text = fetch_l2_snapshot(\"okx\", market_type, symbol, Some(3)).unwrap();\n    assert!(text.starts_with('{'));\n}\n\n#[test_case(MarketType::InverseFuture, \"BTC-USD-221230\")]\n#[test_case(MarketType::LinearFuture, \"BTC-USDT-221230\")]\n#[test_case(MarketType::InverseSwap, \"BTC-USD-SWAP\")]\n#[test_case(MarketType::LinearSwap, \"BTC-USDT-SWAP\")]\n#[test_case(MarketType::EuropeanOption, \"BTC-USD-221230-10000-P\")]\nfn test_open_interest(market_type: MarketType, symbol: &str) {\n    let text = fetch_open_interest(\"okx\", market_type, Some(symbol)).unwrap();\n    let json_obj = serde_json::from_str::<HashMap<String, Value>>(&text).unwrap();\n    let arr = json_obj.get(\"data\").unwrap().as_array().unwrap();\n    assert!(!arr.is_empty());\n}\n\n#[cfg(test)]\nmod okex_swap {\n    use std::collections::HashMap;\n\n    use crypto_rest_client::OkxRestClient;\n    use serde_json::Value;\n\n    #[test]\n    fn test_trades() {\n        let text = OkxRestClient::fetch_trades(\"BTC-USDT-SWAP\").unwrap();\n        let json_obj = serde_json::from_str::<HashMap<String, Value>>(&text).unwrap();\n        let code = json_obj.get(\"code\").unwrap().as_str().unwrap();\n        assert_eq!(\"0\", code);\n    }\n\n    #[test]\n    fn test_option_underlying() {\n        let arr = OkxRestClient::fetch_option_underlying().unwrap();\n        assert!(!arr.is_empty());\n    }\n}\n"
  },
  {
    "path": "crypto-rest-client/tests/zb.rs",
    "content": "use std::collections::HashMap;\n\nuse crypto_market_type::MarketType;\nuse crypto_rest_client::fetch_l2_snapshot;\nuse serde_json::Value;\n\n#[test]\nfn test_spot_l2_snapshot() {\n    let text = fetch_l2_snapshot(\"zb\", MarketType::Spot, \"btc_usdt\", Some(3)).unwrap();\n    let obj = serde_json::from_str::<HashMap<String, Value>>(&text).unwrap();\n\n    assert!(!obj.get(\"asks\").unwrap().as_array().unwrap().is_empty());\n    assert!(!obj.get(\"bids\").unwrap().as_array().unwrap().is_empty());\n}\n\n#[test]\nfn test_swap_l2_snapshot() {\n    let text = fetch_l2_snapshot(\"zb\", MarketType::LinearSwap, \"BTC_USDT\", Some(3)).unwrap();\n    let obj = serde_json::from_str::<HashMap<String, Value>>(&text).unwrap();\n\n    assert_eq!(10000, obj[\"code\"].as_i64().unwrap());\n\n    let data = obj.get(\"data\").unwrap().as_object().unwrap();\n    assert!(!data.get(\"asks\").unwrap().as_array().unwrap().is_empty());\n    assert!(!data.get(\"bids\").unwrap().as_array().unwrap().is_empty());\n}\n"
  },
  {
    "path": "crypto-rest-client/tests/zbg.rs",
    "content": "use std::collections::HashMap;\n\nuse crypto_market_type::MarketType;\nuse crypto_rest_client::{fetch_l2_snapshot, fetch_open_interest};\nuse serde_json::Value;\nuse test_case::test_case;\n\n#[test_case(MarketType::Spot, \"btc_usdt\")]\n#[test_case(MarketType::InverseSwap, \"BTC_USD-R\")]\n#[test_case(MarketType::LinearSwap, \"BTC_USDT\")]\nfn test_l2_snapshot(market_type: MarketType, symbol: &str) {\n    let text = fetch_l2_snapshot(\"zbg\", market_type, symbol, Some(3)).unwrap();\n    let obj = serde_json::from_str::<HashMap<String, Value>>(&text).unwrap();\n\n    let code =\n        obj.get(\"resMsg\").unwrap().as_object().unwrap().get(\"code\").unwrap().as_str().unwrap();\n    if code == \"6001\" || code == \"6020\" {\n        // system in maintainance\n        return;\n    }\n\n    assert_eq!(\"1\", code);\n\n    let data = obj.get(\"datas\").unwrap().as_object().unwrap();\n    assert!(data.get(\"asks\").unwrap().as_array().is_some());\n    assert!(data.get(\"bids\").unwrap().as_array().is_some());\n}\n\n#[test_case(MarketType::InverseSwap, \"BTC_USD-R\")]\n#[test_case(MarketType::LinearSwap, \"BTC_USDT\")]\nfn test_open_interest(market_type: MarketType, symbol: &str) {\n    let text = fetch_open_interest(\"zbg\", market_type, Some(symbol));\n    match text {\n        Ok(text) => {\n            let obj = serde_json::from_str::<HashMap<String, Value>>(&text).unwrap();\n            assert!(obj.contains_key(\"datas\"));\n        }\n        Err(e) => {\n            eprintln!(\"Unable to connect to ZBG API: {e}\")\n        }\n    }\n}\n"
  },
  {
    "path": "crypto-ws-client/Cargo.toml",
    "content": "[package]\nname = \"crypto-ws-client\"\nversion = \"4.12.11\"\nauthors = [\"soulmachine <soulmachine@gmail.com>\"]\nedition = \"2021\"\ndescription = \"A versatile websocket client that supports many cryptocurrency exchanges.\"\nlicense = \"Apache-2.0\"\nrepository = \"https://github.com/crypto-crawler/crypto-crawler-rs/tree/main/crypto-ws-client\"\nkeywords = [\"cryptocurrency\", \"blockchain\", \"trading\", \"websocket\"]\n\n[dependencies]\nasync-trait = \"0.1.64\"\nflate2 = \"1.0.25\"\nfutures-util = \"0.3.26\"\ngovernor = \"0.5.1\"\nnonzero_ext = \"0.3.0\"\nlog = \"0.4.17\"\nrand = \"0.8.5\"\nreqwest = { version = \"0.11.14\", features = [\"gzip\"] }\nserde_json = \"1.0.93\"\ntokio = { version = \"1.25.0\", features = [\"rt-multi-thread\", \"time\", \"sync\", \"macros\"] }\ntokio-tungstenite = { version = \"0.18.0\", features = [\"rustls-tls-native-roots\"] }\nfast-socks5 = \"0.8.1\"\n\n[dev-dependencies]\ntokio = { version = \"1.25.0\", features = [\"test-util\"] }\n"
  },
  {
    "path": "crypto-ws-client/README.md",
    "content": "# crypto-ws-client\n\n[![](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)\n[![](https://img.shields.io/crates/v/crypto-ws-client.svg)](https://crates.io/crates/crypto-ws-client)\n[![](https://docs.rs/crypto-ws-client/badge.svg)](https://docs.rs/crypto-ws-client)\n==========\n\nA versatile websocket client that supports many cryptocurrency exchanges.\n\n## Usage\n\n```rust\nuse crypto_ws_client::{BinanceSpotWSClient, WSClient};\n\n#[tokio::main]\nasync fn main() {\n    let (tx, rx) = std::sync::mpsc::channel();\n    tokio::task::spawn(async move {\n        let symbols = vec![\"BTCUSDT\".to_string(), \"ETHUSDT\".to_string()];\n        let ws_client = BinanceSpotWSClient::new(tx, None).await;\n        ws_client.subscribe_trade(&symbols).await;\n        // run for 5 seconds\n        let _ = tokio::time::timeout(std::time::Duration::from_secs(5), ws_client.run()).await;\n        ws_client.close();\n    });\n\n    for msg in rx {\n        println!(\"{}\", msg);\n    }\n}\n```\n"
  },
  {
    "path": "crypto-ws-client/src/clients/binance.rs",
    "content": "use async_trait::async_trait;\nuse nonzero_ext::nonzero;\nuse std::{collections::HashMap, num::NonZeroU32};\nuse tokio_tungstenite::tungstenite::Message;\n\nuse crate::{\n    common::{\n        command_translator::CommandTranslator,\n        message_handler::{MessageHandler, MiscMessage},\n        utils::ensure_frame_size,\n        ws_client_internal::WSClientInternal,\n    },\n    WSClient,\n};\nuse log::*;\nuse serde_json::Value;\n\npub(crate) const EXCHANGE_NAME: &str = \"binance\";\n\nconst SPOT_WEBSOCKET_URL: &str = \"wss://stream.binance.com:9443/stream\";\nconst LINEAR_WEBSOCKET_URL: &str = \"wss://fstream.binance.com/stream\";\nconst INVERSE_WEBSOCKET_URL: &str = \"wss://dstream.binance.com/stream\";\n\n// the websocket message size should not exceed 4096 bytes, otherwise\n// you'll get `code: 3001, reason: illegal request`\nconst WS_FRAME_SIZE: usize = 4096;\n\n// WebSocket connections have a limit of 5 incoming messages per second.\n//\n// See:\n//\n// * https://binance-docs.github.io/apidocs/spot/en/#limits\n// * https://binance-docs.github.io/apidocs/futures/en/#websocket-market-streams\n// * https://binance-docs.github.io/apidocs/delivery/en/#websocket-market-streams\nconst UPLINK_LIMIT: (NonZeroU32, std::time::Duration) =\n    (nonzero!(5u32), std::time::Duration::from_secs(1));\n\n// Internal unified client\npub struct BinanceWSClient<const MARKET_TYPE: char> {\n    client: WSClientInternal<BinanceMessageHandler>,\n    translator: BinanceCommandTranslator,\n}\n\n/// Binance Spot market.\n///\n///   * WebSocket API doc: <https://binance-docs.github.io/apidocs/spot/en/>\n///   * Trading at: <https://www.binance.com/en/trade/BTC_USDT>\npub type BinanceSpotWSClient = BinanceWSClient<'S'>;\n\n/// Binance Coin-margined Future and Swap markets.\n///\n///   * WebSocket API doc: <https://binance-docs.github.io/apidocs/delivery/en/>\n///   * Trading at: <https://www.binance.com/en/delivery/btcusd_quarter>\npub type BinanceInverseWSClient = BinanceWSClient<'I'>;\n\n/// Binance USDT-margined Future and Swap markets.\n///\n///   * WebSocket API doc: <https://binance-docs.github.io/apidocs/futures/en/>\n///   * Trading at: <https://www.binance.com/en/futures/BTC_USDT>\npub type BinanceLinearWSClient = BinanceWSClient<'L'>;\n\nimpl<const MARKET_TYPE: char> BinanceWSClient<MARKET_TYPE> {\n    pub async fn new(tx: std::sync::mpsc::Sender<String>, url: Option<&str>) -> Self {\n        let real_url = match url {\n            Some(endpoint) => endpoint,\n            None => {\n                if MARKET_TYPE == 'S' {\n                    SPOT_WEBSOCKET_URL\n                } else if MARKET_TYPE == 'I' {\n                    INVERSE_WEBSOCKET_URL\n                } else if MARKET_TYPE == 'L' {\n                    LINEAR_WEBSOCKET_URL\n                } else {\n                    panic!(\"Unknown market type {MARKET_TYPE}\");\n                }\n            }\n        };\n        BinanceWSClient {\n            client: WSClientInternal::connect(\n                EXCHANGE_NAME,\n                real_url,\n                BinanceMessageHandler {},\n                Some(UPLINK_LIMIT),\n                tx,\n            )\n            .await,\n            translator: BinanceCommandTranslator { market_type: MARKET_TYPE },\n        }\n    }\n}\n\n#[async_trait]\nimpl<const URL: char> WSClient for BinanceWSClient<URL> {\n    async fn subscribe_trade(&self, symbols: &[String]) {\n        let topics = symbols\n            .iter()\n            .map(|symbol| (\"aggTrade\".to_string(), symbol.to_string()))\n            .collect::<Vec<(String, String)>>();\n        self.subscribe(&topics).await;\n    }\n\n    async fn subscribe_orderbook(&self, symbols: &[String]) {\n        let topics = symbols\n            .iter()\n            .map(|symbol| (\"depth@100ms\".to_string(), symbol.to_string()))\n            .collect::<Vec<(String, String)>>();\n        self.subscribe(&topics).await;\n    }\n\n    async fn subscribe_orderbook_topk(&self, symbols: &[String]) {\n        let topics = symbols\n            .iter()\n            .map(|symbol| (\"depth20\".to_string(), symbol.to_string()))\n            .collect::<Vec<(String, String)>>();\n        self.subscribe(&topics).await;\n    }\n\n    async fn subscribe_l3_orderbook(&self, _symbols: &[String]) {\n        panic!(\"{EXCHANGE_NAME} does NOT have the level3 websocket channel\");\n    }\n\n    async fn subscribe_ticker(&self, symbols: &[String]) {\n        let topics = symbols\n            .iter()\n            .map(|symbol| (\"ticker\".to_string(), symbol.to_string()))\n            .collect::<Vec<(String, String)>>();\n        self.subscribe(&topics).await;\n    }\n\n    async fn subscribe_bbo(&self, symbols: &[String]) {\n        let topics = symbols\n            .iter()\n            .map(|symbol| (\"bookTicker\".to_string(), symbol.to_string()))\n            .collect::<Vec<(String, String)>>();\n        self.subscribe(&topics).await;\n    }\n\n    async fn subscribe_candlestick(&self, symbol_interval_list: &[(String, usize)]) {\n        let commands =\n            self.translator.translate_to_candlestick_commands(true, symbol_interval_list);\n        self.client.send(&commands).await;\n    }\n\n    async fn subscribe(&self, topics: &[(String, String)]) {\n        let commands = self.translator.translate_to_commands(true, topics);\n        self.client.send(&commands).await;\n    }\n\n    async fn unsubscribe(&self, topics: &[(String, String)]) {\n        let commands = self.translator.translate_to_commands(false, topics);\n        self.client.send(&commands).await;\n    }\n\n    async fn send(&self, commands: &[String]) {\n        self.client.send(commands).await;\n    }\n\n    async fn run(&self) {\n        self.client.run().await;\n    }\n\n    async fn close(&self) {\n        self.client.close().await;\n    }\n}\n\nstruct BinanceMessageHandler {}\nstruct BinanceCommandTranslator {\n    market_type: char,\n}\n\nimpl BinanceCommandTranslator {\n    fn topics_to_command(topics: &[(String, String)], subscribe: bool) -> String {\n        let raw_topics = topics\n            .iter()\n            .map(|(topic, symbol)| format!(\"{}@{}\", symbol.to_lowercase(), topic))\n            .collect::<Vec<String>>();\n        format!(\n            r#\"{{\"id\":9527,\"method\":\"{}\",\"params\":{}}}\"#,\n            if subscribe { \"SUBSCRIBE\" } else { \"UNSUBSCRIBE\" },\n            serde_json::to_string(&raw_topics).unwrap()\n        )\n    }\n\n    // see https://binance-docs.github.io/apidocs/futures/en/#kline-candlestick-streams\n    fn to_candlestick_raw_channel(interval: usize) -> String {\n        let interval_str = match interval {\n            60 => \"1m\",\n            180 => \"3m\",\n            300 => \"5m\",\n            900 => \"15m\",\n            1800 => \"30m\",\n            3600 => \"1h\",\n            7200 => \"2h\",\n            14400 => \"4h\",\n            21600 => \"6h\",\n            28800 => \"8h\",\n            43200 => \"12h\",\n            86400 => \"1d\",\n            259200 => \"3d\",\n            604800 => \"1w\",\n            2592000 => \"1M\",\n            _ => panic!(\"Binance has intervals 1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,1M\"),\n        };\n        format!(\"kline_{interval_str}\")\n    }\n}\n\nimpl MessageHandler for BinanceMessageHandler {\n    fn handle_message(&mut self, msg: &str) -> MiscMessage {\n        let resp = serde_json::from_str::<HashMap<String, Value>>(msg);\n        if resp.is_err() {\n            error!(\"{} is not a JSON string, {}\", msg, EXCHANGE_NAME);\n            return MiscMessage::Other;\n        }\n        let obj = resp.unwrap();\n\n        if obj.contains_key(\"error\") {\n            panic!(\"Received {msg} from {EXCHANGE_NAME}\");\n        } else if obj.contains_key(\"stream\") && obj.contains_key(\"data\") {\n            MiscMessage::Normal\n        } else {\n            if let Some(result) = obj.get(\"result\") {\n                if serde_json::Value::Null != *result {\n                    panic!(\"Received {msg} from {EXCHANGE_NAME}\");\n                } else {\n                    info!(\"Received {} from {}\", msg, EXCHANGE_NAME);\n                }\n            } else {\n                warn!(\"Received {} from {}\", msg, EXCHANGE_NAME);\n            }\n            MiscMessage::Other\n        }\n    }\n\n    fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)> {\n        // https://binance-docs.github.io/apidocs/spot/en/#websocket-market-streams\n        // https://binance-docs.github.io/apidocs/futures/en/#websocket-market-streams\n        // https://binance-docs.github.io/apidocs/delivery/en/#websocket-market-streams\n        // The websocket server will send a ping frame every 3 minutes. If the websocket\n        // server does not receive a pong frame back from the connection within\n        // a 10 minute period, the connection will be disconnected. Unsolicited\n        // pong frames are allowed. Send unsolicited pong frames per 3 minutes\n        Some((Message::Pong(Vec::new()), 180))\n    }\n}\n\nimpl CommandTranslator for BinanceCommandTranslator {\n    fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec<String> {\n        let max_num_topics = if self.market_type == 'S' {\n            // https://binance-docs.github.io/apidocs/spot/en/#websocket-limits\n            1024\n        } else {\n            // https://binance-docs.github.io/apidocs/futures/en/#websocket-market-streams\n            // https://binance-docs.github.io/apidocs/delivery/en/#websocket-market-streams\n            200\n        };\n        ensure_frame_size(\n            topics,\n            subscribe,\n            Self::topics_to_command,\n            WS_FRAME_SIZE,\n            Some(max_num_topics),\n        )\n    }\n\n    fn translate_to_candlestick_commands(\n        &self,\n        subscribe: bool,\n        symbol_interval_list: &[(String, usize)],\n    ) -> Vec<String> {\n        let topics = symbol_interval_list\n            .iter()\n            .map(|(symbol, interval)| {\n                let channel = Self::to_candlestick_raw_channel(*interval);\n                (channel, symbol.to_lowercase())\n            })\n            .collect::<Vec<(String, String)>>();\n        self.translate_to_commands(subscribe, &topics)\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use crate::common::command_translator::CommandTranslator;\n\n    #[test]\n    fn test_one_topic() {\n        let translator = super::BinanceCommandTranslator { market_type: 'S' };\n        let commands = translator\n            .translate_to_commands(true, &[(\"aggTrade\".to_string(), \"BTCUSDT\".to_string())]);\n\n        assert_eq!(1, commands.len());\n        assert_eq!(\n            r#\"{\"id\":9527,\"method\":\"SUBSCRIBE\",\"params\":[\"btcusdt@aggTrade\"]}\"#,\n            commands[0]\n        );\n    }\n\n    #[test]\n    fn test_two_topics() {\n        let translator = super::BinanceCommandTranslator { market_type: 'S' };\n        let commands = translator.translate_to_commands(\n            true,\n            &[\n                (\"aggTrade\".to_string(), \"BTCUSDT\".to_string()),\n                (\"ticker\".to_string(), \"BTCUSDT\".to_string()),\n            ],\n        );\n\n        assert_eq!(1, commands.len());\n        assert_eq!(\n            r#\"{\"id\":9527,\"method\":\"SUBSCRIBE\",\"params\":[\"btcusdt@aggTrade\",\"btcusdt@ticker\"]}\"#,\n            commands[0]\n        );\n    }\n}\n"
  },
  {
    "path": "crypto-ws-client/src/clients/binance_option.rs",
    "content": "use async_trait::async_trait;\nuse std::collections::HashMap;\nuse tokio_tungstenite::tungstenite::Message;\n\nuse crate::{\n    clients::common_traits::{\n        Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO,\n    },\n    common::{\n        command_translator::CommandTranslator,\n        message_handler::{MessageHandler, MiscMessage},\n        ws_client_internal::WSClientInternal,\n    },\n    WSClient,\n};\nuse log::*;\nuse serde_json::Value;\n\npub(crate) const EXCHANGE_NAME: &str = \"binance\";\n\npub(super) const WEBSOCKET_URL: &str = \"wss://stream.opsnest.com/stream\";\n\n/// Binance Option market\n///\n///   * WebSocket API doc: <https://binance-docs.github.io/apidocs/voptions/en/>\n///   * Trading at: <https://voptions.binance.com/en>\npub struct BinanceOptionWSClient {\n    client: WSClientInternal<BinanceOptionMessageHandler>,\n    translator: BinanceOptionCommandTranslator,\n}\n\nimpl_new_constructor!(\n    BinanceOptionWSClient,\n    EXCHANGE_NAME,\n    WEBSOCKET_URL,\n    BinanceOptionMessageHandler {},\n    BinanceOptionCommandTranslator {}\n);\n\nimpl_trait!(Trade, BinanceOptionWSClient, subscribe_trade, \"trade\");\nimpl_trait!(Ticker, BinanceOptionWSClient, subscribe_ticker, \"ticker\");\nimpl_trait!(BBO, BinanceOptionWSClient, subscribe_bbo, \"bookTicker\");\n#[rustfmt::skip]\nimpl_trait!(OrderBook, BinanceOptionWSClient, subscribe_orderbook, \"depth@100ms\");\n#[rustfmt::skip]\nimpl_trait!(OrderBookTopK, BinanceOptionWSClient, subscribe_orderbook_topk, \"depth10\");\nimpl_candlestick!(BinanceOptionWSClient);\npanic_l3_orderbook!(BinanceOptionWSClient);\n\nimpl_ws_client_trait!(BinanceOptionWSClient);\n\nstruct BinanceOptionMessageHandler {}\nstruct BinanceOptionCommandTranslator {}\n\nimpl BinanceOptionCommandTranslator {\n    fn topics_to_command(topics: &[(String, String)], subscribe: bool) -> String {\n        let raw_topics = topics\n            .iter()\n            .map(|(topic, symbol)| format!(\"{symbol}@{topic}\"))\n            .collect::<Vec<String>>();\n        format!(\n            r#\"{{\"id\":9527,\"method\":\"{}\",\"params\":{}}}\"#,\n            if subscribe { \"SUBSCRIBE\" } else { \"UNSUBSCRIBE\" },\n            serde_json::to_string(&raw_topics).unwrap()\n        )\n    }\n\n    // see https://binance-docs.github.io/apidocs/voptions/en/#payload-candle\n    fn to_candlestick_raw_channel(interval: usize) -> String {\n        let interval_str = match interval {\n            60 => \"1m\",\n            300 => \"5m\",\n            900 => \"15m\",\n            1800 => \"30m\",\n            3600 => \"1h\",\n            14400 => \"4h\",\n            86400 => \"1d\",\n            604800 => \"1w\",\n            _ => panic!(\"Binance Option has intervals 1m,5m,15m,30m,1h4h,1d,1w\"),\n        };\n        format!(\"kline_{interval_str}\")\n    }\n}\n\nimpl MessageHandler for BinanceOptionMessageHandler {\n    fn handle_message(&mut self, msg: &str) -> MiscMessage {\n        if msg == r#\"{\"id\":9527}\"# {\n            return MiscMessage::Other;\n        } else if msg == r#\"{\"event\":\"pong\"}\"# {\n            return MiscMessage::Pong;\n        }\n\n        let resp = serde_json::from_str::<HashMap<String, Value>>(msg);\n        if resp.is_err() {\n            error!(\"{} is not a JSON string, {}\", msg, EXCHANGE_NAME);\n            return MiscMessage::Other;\n        }\n        let obj = resp.unwrap();\n\n        if obj.contains_key(\"code\") {\n            panic!(\"Received {msg} from {EXCHANGE_NAME}\");\n        }\n\n        if let Some(result) = obj.get(\"result\") {\n            if serde_json::Value::Null == *result {\n                return MiscMessage::Other;\n            }\n        }\n\n        if !obj.contains_key(\"stream\") || !obj.contains_key(\"data\") {\n            warn!(\"Received {} from {}\", msg, EXCHANGE_NAME);\n            return MiscMessage::Other;\n        }\n\n        MiscMessage::Normal\n    }\n\n    fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)> {\n        // https://binance-docs.github.io/apidocs/voptions/en/#push-websocket-account-info\n        // The client will send a ping frame every 2 minutes. If the websocket server\n        // does not receive a ping frame back from the connection within a 2\n        // minute period, the connection will be disconnected. Unsolicited ping\n        // frames are allowed.\n        Some((Message::Text(r#\"{\"event\":\"ping\"}\"#.to_string()), 120))\n    }\n}\n\nimpl CommandTranslator for BinanceOptionCommandTranslator {\n    fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec<String> {\n        let command = Self::topics_to_command(topics, subscribe);\n        vec![command]\n    }\n\n    fn translate_to_candlestick_commands(\n        &self,\n        subscribe: bool,\n        symbol_interval_list: &[(String, usize)],\n    ) -> Vec<String> {\n        let topics = symbol_interval_list\n            .iter()\n            .map(|(symbol, interval)| {\n                let channel = Self::to_candlestick_raw_channel(*interval);\n                (channel, symbol.to_string())\n            })\n            .collect::<Vec<(String, String)>>();\n        self.translate_to_commands(subscribe, &topics)\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use crate::common::command_translator::CommandTranslator;\n\n    #[test]\n    fn test_one_topic() {\n        let translator = super::BinanceOptionCommandTranslator {};\n        let commands = translator.translate_to_commands(\n            true,\n            &[(\"trade\".to_string(), \"BTC-220429-50000-C\".to_string())],\n        );\n\n        assert_eq!(1, commands.len());\n        assert_eq!(\n            r#\"{\"id\":9527,\"method\":\"SUBSCRIBE\",\"params\":[\"BTC-220429-50000-C@trade\"]}\"#,\n            commands[0]\n        );\n    }\n\n    #[test]\n    fn test_two_topics() {\n        let translator = super::BinanceOptionCommandTranslator {};\n        let commands = translator.translate_to_commands(\n            true,\n            &[\n                (\"trade\".to_string(), \"BTC-220429-50000-C\".to_string()),\n                (\"ticker\".to_string(), \"BTC-220429-50000-C\".to_string()),\n            ],\n        );\n\n        assert_eq!(1, commands.len());\n        assert_eq!(\n            r#\"{\"id\":9527,\"method\":\"SUBSCRIBE\",\"params\":[\"BTC-220429-50000-C@trade\",\"BTC-220429-50000-C@ticker\"]}\"#,\n            commands[0]\n        );\n    }\n}\n"
  },
  {
    "path": "crypto-ws-client/src/clients/bitfinex.rs",
    "content": "use async_trait::async_trait;\nuse std::{\n    collections::{BTreeMap, HashMap},\n    time::Duration,\n};\nuse tokio_tungstenite::tungstenite::Message;\n\nuse crate::{\n    clients::common_traits::{\n        Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO,\n    },\n    common::{\n        command_translator::CommandTranslator,\n        message_handler::{MessageHandler, MiscMessage},\n        ws_client_internal::WSClientInternal,\n    },\n    WSClient,\n};\n\nuse log::*;\nuse serde_json::Value;\n\npub(super) const EXCHANGE_NAME: &str = \"bitfinex\";\n\nconst WEBSOCKET_URL: &str = \"wss://api-pub.bitfinex.com/ws/2\";\n\n/// The WebSocket client for Bitfinex, including all markets.\n///\n/// * WebSocket API doc: <https://docs.bitfinex.com/docs/ws-general>\n/// * Spot: <https://trading.bitfinex.com/trading>\n/// * Swap: <https://trading.bitfinex.com/t/BTCF0:USTF0>\n/// * Funding: <https://trading.bitfinex.com/funding>\npub struct BitfinexWSClient {\n    client: WSClientInternal<BitfinexMessageHandler>,\n    translator: BitfinexCommandTranslator, // used by close() and run()\n}\n\nimpl_new_constructor!(\n    BitfinexWSClient,\n    EXCHANGE_NAME,\n    WEBSOCKET_URL,\n    BitfinexMessageHandler { channel_id_meta: HashMap::new() },\n    BitfinexCommandTranslator {}\n);\n\nimpl_trait!(Trade, BitfinexWSClient, subscribe_trade, \"trades\");\nimpl_trait!(Ticker, BitfinexWSClient, subscribe_ticker, \"ticker\");\nimpl_candlestick!(BitfinexWSClient);\n\npanic_bbo!(BitfinexWSClient);\npanic_l2_topk!(BitfinexWSClient);\n\n#[async_trait]\nimpl OrderBook for BitfinexWSClient {\n    async fn subscribe_orderbook(&self, symbols: &[String]) {\n        let commands = symbols\n            .iter()\n            .map(|symbol| {\n                format!(r#\"{{\"event\": \"subscribe\",\"channel\": \"book\",\"symbol\": \"{symbol}\",\"prec\": \"P0\",\"frec\": \"F0\",\"len\":25}}\"#,\n                )\n            })\n            .collect::<Vec<String>>();\n\n        self.send(&commands).await;\n    }\n}\n\n#[async_trait]\nimpl Level3OrderBook for BitfinexWSClient {\n    async fn subscribe_l3_orderbook(&self, symbols: &[String]) {\n        let commands = symbols\n            .iter()\n            .map(|symbol| {\n                format!(r#\"{{\"event\": \"subscribe\",\"channel\": \"book\",\"symbol\": \"{symbol}\",\"prec\": \"R0\",\"len\": 250}}\"#,\n                )\n            })\n            .collect::<Vec<String>>();\n\n        self.send(&commands).await;\n    }\n}\n\nimpl_ws_client_trait!(BitfinexWSClient);\n\nstruct BitfinexMessageHandler {\n    channel_id_meta: HashMap<i64, String>, // CHANNEL_ID information\n}\nstruct BitfinexCommandTranslator {}\n\nimpl BitfinexCommandTranslator {\n    fn topic_to_command(channel: &str, symbol: &str, subscribe: bool) -> String {\n        format!(\n            r#\"{{\"event\": \"{}\", \"channel\": \"{}\", \"symbol\": \"{}\"}}\"#,\n            if subscribe { \"subscribe\" } else { \"unsubscribe\" },\n            channel,\n            symbol\n        )\n    }\n    fn to_candlestick_command(symbol: &str, interval: usize, subscribe: bool) -> String {\n        let interval_str = match interval {\n            60 => \"1m\",\n            300 => \"5m\",\n            900 => \"15m\",\n            1800 => \"30m\",\n            3600 => \"1h\",\n            10800 => \"3h\",\n            21600 => \"6h\",\n            43200 => \"12h\",\n            86400 => \"1D\",\n            604800 => \"7D\",\n            1209600 => \"14D\",\n            2592000 => \"1M\",\n            _ => panic!(\"Bitfinex available intervals 1m,5m,15m,30m,1h,3h,6h,12h,1D,7D,14D,1M\"),\n        };\n\n        format!(\n            r#\"{{\"event\": \"{}\",\"channel\": \"candles\",\"key\": \"trade:{}:{}\"}}\"#,\n            if subscribe { \"subscribe\" } else { \"unsubscribe\" },\n            interval_str,\n            symbol\n        )\n    }\n}\n\nimpl MessageHandler for BitfinexMessageHandler {\n    fn handle_message(&mut self, txt: &str) -> MiscMessage {\n        if txt.starts_with('{') {\n            let obj = serde_json::from_str::<HashMap<String, Value>>(txt).unwrap();\n            let event = obj.get(\"event\").unwrap().as_str().unwrap();\n            match event {\n                \"error\" => {\n                    let code = obj.get(\"code\").unwrap().as_i64().unwrap();\n                    match code {\n                        10301 | 10401 => {\n                            // 10301: Already subscribed\n                            // 10401: Not subscribed\n                            // 10000: Unknown event\n                            warn!(\"{} from {}\", txt, EXCHANGE_NAME);\n                        }\n                        10300 | 10400 | 10302 => {\n                            // 10300, 10400:Subscription failed\n                            // 10302: Unknown channel\n                            // 10001: Unknown pair\n                            // 10305: Reached limit of open channels\n                            error!(\"{} from {}\", txt, EXCHANGE_NAME);\n                            panic!(\"{txt} from {EXCHANGE_NAME}\");\n                        }\n                        _ => warn!(\"{} from {}\", txt, EXCHANGE_NAME),\n                    }\n                    MiscMessage::Other\n                }\n                \"info\" => {\n                    if obj.get(\"version\").is_some() {\n                        // 1 for operative, 0 for maintenance\n                        let status = obj\n                            .get(\"platform\")\n                            .unwrap()\n                            .as_object()\n                            .unwrap()\n                            .get(\"status\")\n                            .unwrap()\n                            .as_i64()\n                            .unwrap();\n                        if status == 0 {\n                            std::thread::sleep(Duration::from_secs(15));\n                            MiscMessage::Reconnect\n                        } else {\n                            MiscMessage::Other\n                        }\n                    } else {\n                        let code = obj.get(\"code\").unwrap().as_i64().unwrap();\n                        match code {\n                            20051 => {\n                                // Stop/Restart Websocket Server (please reconnect)\n                                // self.reconnect();\n                                error!(\"Stop/Restart Websocket Server, exiting now...\");\n                                MiscMessage::Reconnect // fail fast, pm2 will restart\n                            }\n                            20060 => {\n                                // Entering in Maintenance mode. Please pause any activity and\n                                // resume after receiving the info\n                                // message 20061 (it should take 120 seconds\n                                // at most).\n                                std::thread::sleep(Duration::from_secs(15));\n                                MiscMessage::Other\n                            }\n                            20061 => {\n                                // Maintenance ended. You can resume normal activity. It is advised\n                                // to unsubscribe/subscribe again all channels.\n                                MiscMessage::Reconnect\n                            }\n                            _ => {\n                                info!(\"{} from {}\", txt, EXCHANGE_NAME);\n                                MiscMessage::Other\n                            }\n                        }\n                    }\n                }\n                \"pong\" => {\n                    debug!(\"{} from {}\", txt, EXCHANGE_NAME);\n                    MiscMessage::Pong\n                }\n                \"conf\" => {\n                    warn!(\"{} from {}\", txt, EXCHANGE_NAME);\n                    MiscMessage::Other\n                }\n                \"subscribed\" => {\n                    let mut obj_sorted = BTreeMap::<String, Value>::new();\n                    for (key, value) in obj.iter() {\n                        obj_sorted.insert(key.to_string(), value.clone());\n                    }\n                    let chan_id = obj.get(\"chanId\").unwrap().as_i64().unwrap();\n                    obj_sorted.remove(\"event\");\n                    obj_sorted.remove(\"chanId\");\n                    obj_sorted.remove(\"pair\");\n                    self.channel_id_meta\n                        .insert(chan_id, serde_json::to_string(&obj_sorted).unwrap());\n                    MiscMessage::Other\n                }\n                \"unsubscribed\" => {\n                    let chan_id = obj.get(\"chanId\").unwrap().as_i64().unwrap();\n                    self.channel_id_meta.remove(&chan_id);\n                    MiscMessage::Other\n                }\n                _ => MiscMessage::Other,\n            }\n        } else {\n            debug_assert!(txt.starts_with('['));\n            let arr = serde_json::from_str::<Vec<Value>>(txt).unwrap();\n            if arr.is_empty() {\n                MiscMessage::Other // ignore empty array\n            } else if arr.len() == 2 && arr[1].as_str().unwrap_or(\"null\") == \"hb\" {\n                // If there is no activity in the channel for 15 seconds, the Websocket server\n                // will send you a heartbeat message in this format.\n                // see <https://docs.bitfinex.com/docs/ws-general#heartbeating>\n                MiscMessage::WebSocket(Message::Text(r#\"{\"event\":\"ping\"}\"#.to_string()))\n            } else {\n                // replace CHANNEL_ID with meta info\n                let i = txt.find(',').unwrap(); // first comma, for example, te, tu, see https://blog.bitfinex.com/api/websocket-api-update/\n                let channel_id = (txt[1..i]).parse::<i64>().unwrap();\n                if let Some(channel_info) = self.channel_id_meta.get(&channel_id) {\n                    let new_txt = format!(\"[{}{}\", channel_info, &txt[i..]);\n                    MiscMessage::Mutated(new_txt)\n                } else {\n                    MiscMessage::Other\n                }\n            }\n        }\n    }\n\n    fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)> {\n        // If there is no activity in the channel for 15 seconds, the Websocket server\n        // will send you a heartbeat message, see https://docs.bitfinex.com/docs/ws-general#heartbeating\n        None\n    }\n}\n\nimpl CommandTranslator for BitfinexCommandTranslator {\n    fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec<String> {\n        topics\n            .iter()\n            .map(|(channel, symbol)| Self::topic_to_command(channel, symbol, subscribe))\n            .collect()\n    }\n\n    fn translate_to_candlestick_commands(\n        &self,\n        subscribe: bool,\n        symbol_interval_list: &[(String, usize)],\n    ) -> Vec<String> {\n        symbol_interval_list\n            .iter()\n            .map(|(symbol, interval)| Self::to_candlestick_command(symbol, *interval, subscribe))\n            .collect::<Vec<String>>()\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use crate::common::command_translator::CommandTranslator;\n\n    #[test]\n    fn test_spot_command() {\n        let translator = super::BitfinexCommandTranslator {};\n        let commands = translator\n            .translate_to_commands(true, &[(\"trades\".to_string(), \"tBTCUSD\".to_string())]);\n\n        assert_eq!(1, commands.len());\n        assert_eq!(\n            r#\"{\"event\": \"subscribe\", \"channel\": \"trades\", \"symbol\": \"tBTCUSD\"}\"#,\n            commands[0]\n        );\n    }\n\n    #[test]\n    fn test_swap_command() {\n        let translator = super::BitfinexCommandTranslator {};\n        let commands = translator\n            .translate_to_commands(true, &[(\"trades\".to_string(), \"tBTCF0:USTF0\".to_string())]);\n\n        assert_eq!(1, commands.len());\n        assert_eq!(\n            r#\"{\"event\": \"subscribe\", \"channel\": \"trades\", \"symbol\": \"tBTCF0:USTF0\"}\"#,\n            commands[0]\n        );\n    }\n}\n"
  },
  {
    "path": "crypto-ws-client/src/clients/bitget/bitget_spot.rs",
    "content": "use async_trait::async_trait;\n\nuse crate::{\n    clients::common_traits::{\n        Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO,\n    },\n    common::{command_translator::CommandTranslator, ws_client_internal::WSClientInternal},\n    WSClient,\n};\n\nuse super::{\n    utils::{BitgetCommandTranslator, BitgetMessageHandler, UPLINK_LIMIT},\n    EXCHANGE_NAME,\n};\n\nconst WEBSOCKET_URL: &str = \"wss://ws.bitget.com/spot/v1/stream\";\n\n/// The WebSocket client for Bitget Spot market.\n///\n/// * WebSocket API doc: <https://bitgetlimited.github.io/apidoc/en/spot/#websocketapi>\n/// * Trading at: <https://www.bitget.com/en/spot/>\npub struct BitgetSpotWSClient {\n    client: WSClientInternal<BitgetMessageHandler>,\n    translator: BitgetCommandTranslator<'S'>,\n}\n\nimpl BitgetSpotWSClient {\n    pub async fn new(tx: std::sync::mpsc::Sender<String>, url: Option<&str>) -> Self {\n        let real_url = match url {\n            Some(endpoint) => endpoint,\n            None => WEBSOCKET_URL,\n        };\n        BitgetSpotWSClient {\n            client: WSClientInternal::connect(\n                EXCHANGE_NAME,\n                real_url,\n                BitgetMessageHandler {},\n                Some(UPLINK_LIMIT),\n                tx,\n            )\n            .await,\n            translator: BitgetCommandTranslator::<'S'> {},\n        }\n    }\n}\n\nimpl_trait!(Trade, BitgetSpotWSClient, subscribe_trade, \"trade\");\n#[rustfmt::skip]\nimpl_trait!(OrderBookTopK, BitgetSpotWSClient, subscribe_orderbook_topk, \"books15\");\nimpl_trait!(OrderBook, BitgetSpotWSClient, subscribe_orderbook, \"books\");\nimpl_trait!(Ticker, BitgetSpotWSClient, subscribe_ticker, \"ticker\");\nimpl_candlestick!(BitgetSpotWSClient);\n\npanic_bbo!(BitgetSpotWSClient);\npanic_l3_orderbook!(BitgetSpotWSClient);\n\nimpl_ws_client_trait!(BitgetSpotWSClient);\n"
  },
  {
    "path": "crypto-ws-client/src/clients/bitget/bitget_swap.rs",
    "content": "use async_trait::async_trait;\n\nuse crate::{\n    clients::common_traits::{\n        Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO,\n    },\n    common::{command_translator::CommandTranslator, ws_client_internal::WSClientInternal},\n    WSClient,\n};\n\nuse super::{\n    utils::{BitgetCommandTranslator, BitgetMessageHandler, UPLINK_LIMIT},\n    EXCHANGE_NAME,\n};\n\nconst WEBSOCKET_URL: &str = \"wss://ws.bitget.com/mix/v1/stream\";\n\n/// The WebSocket client for Bitget swap markets.\n///\n/// * WebSocket API doc: <https://bitgetlimited.github.io/apidoc/en/mix/#websocketapi>\n/// * Trading at: <https://www.bitget.com/en/swap/>\npub struct BitgetSwapWSClient {\n    client: WSClientInternal<BitgetMessageHandler>,\n    translator: BitgetCommandTranslator<'M'>,\n}\n\nimpl BitgetSwapWSClient {\n    pub async fn new(tx: std::sync::mpsc::Sender<String>, url: Option<&str>) -> Self {\n        let real_url = match url {\n            Some(endpoint) => endpoint,\n            None => WEBSOCKET_URL,\n        };\n        BitgetSwapWSClient {\n            client: WSClientInternal::connect(\n                EXCHANGE_NAME,\n                real_url,\n                BitgetMessageHandler {},\n                Some(UPLINK_LIMIT),\n                tx,\n            )\n            .await,\n            translator: BitgetCommandTranslator::<'M'> {},\n        }\n    }\n}\n\nimpl_trait!(Trade, BitgetSwapWSClient, subscribe_trade, \"trade\");\n#[rustfmt::skip]\nimpl_trait!(OrderBookTopK, BitgetSwapWSClient, subscribe_orderbook_topk, \"books15\");\nimpl_trait!(OrderBook, BitgetSwapWSClient, subscribe_orderbook, \"books\");\nimpl_trait!(Ticker, BitgetSwapWSClient, subscribe_ticker, \"ticker\");\nimpl_candlestick!(BitgetSwapWSClient);\n\npanic_bbo!(BitgetSwapWSClient);\npanic_l3_orderbook!(BitgetSwapWSClient);\n\nimpl_ws_client_trait!(BitgetSwapWSClient);\n"
  },
  {
    "path": "crypto-ws-client/src/clients/bitget/mod.rs",
    "content": "mod bitget_spot;\nmod bitget_swap;\nmod utils;\n\npub use bitget_spot::BitgetSpotWSClient;\npub use bitget_swap::BitgetSwapWSClient;\n\npub(super) const EXCHANGE_NAME: &str = \"bitget\";\n"
  },
  {
    "path": "crypto-ws-client/src/clients/bitget/utils.rs",
    "content": "use nonzero_ext::nonzero;\nuse std::{\n    collections::{BTreeMap, HashMap},\n    num::NonZeroU32,\n};\nuse tokio_tungstenite::tungstenite::Message;\n\nuse log::*;\nuse serde_json::Value;\n\nuse crate::common::{\n    command_translator::CommandTranslator,\n    message_handler::{MessageHandler, MiscMessage},\n    utils::ensure_frame_size,\n};\n\npub(crate) const EXCHANGE_NAME: &str = \"bitget\";\n\n// The total length of multiple channel can not exceeds 4096 bytes, see:\n// * https://bitgetlimited.github.io/apidoc/en/mix/#subscribe\n// * https://bitgetlimited.github.io/apidoc/en/spot/#subscribe\nconst WS_FRAME_SIZE: usize = 4096;\n\n// Subscription limit: 240 times per hour, see:\n// * https://bitgetlimited.github.io/apidoc/en/mix/#connect\n// * https://bitgetlimited.github.io/apidoc/en/spot/#connect\npub(super) const UPLINK_LIMIT: (NonZeroU32, std::time::Duration) =\n    (nonzero!(240u32), std::time::Duration::from_secs(3600));\n\n// MARKET_TYPE: S for SP, M for MC\npub(super) struct BitgetMessageHandler {}\npub(super) struct BitgetCommandTranslator<const MARKET_TYPE: char> {}\n\nimpl<const MARKET_TYPE: char> BitgetCommandTranslator<MARKET_TYPE> {\n    // doc: https://bitgetlimited.github.io/apidoc/en/spot/#subscribe\n    fn topics_to_command(topics: &[(String, String)], subscribe: bool) -> String {\n        let arr = topics\n            .iter()\n            .map(|t| {\n                let mut map = BTreeMap::new();\n                let (channel, symbol) = t;\n                // websocket doesn't recognize SPBL, DMCBL and UMCBL suffixes\n                let symbol = if let Some(x) = symbol.strip_suffix(\"_SPBL\") {\n                    x\n                } else if let Some(x) = symbol.strip_suffix(\"_DMCBL\") {\n                    x\n                } else if let Some(x) = symbol.strip_suffix(\"_UMCBL\") {\n                    x\n                } else {\n                    symbol\n                };\n                map.insert(\n                    \"instType\".to_string(),\n                    (if MARKET_TYPE == 'S' { \"SP\" } else { \"MC\" }).to_string(),\n                );\n                map.insert(\"channel\".to_string(), channel.to_string());\n                map.insert(\"instId\".to_string(), symbol.to_string());\n                map\n            })\n            .collect::<Vec<BTreeMap<String, String>>>();\n        format!(\n            r#\"{{\"op\":\"{}\",\"args\":{}}}\"#,\n            if subscribe { \"subscribe\" } else { \"unsubscribe\" },\n            serde_json::to_string(&arr).unwrap(),\n        )\n    }\n\n    // https://bitgetlimited.github.io/apidoc/en/spot/#candlesticks-channel\n    // https://bitgetlimited.github.io/apidoc/en/mix/#candlesticks-channel\n    fn to_candlestick_raw_channel(interval: usize) -> &'static str {\n        match interval {\n            60 => \"candle1m\",\n            300 => \"candle5m\",\n            900 => \"candle15m\",\n            1800 => \"candle30m\",\n            3600 => \"candle1H\",\n            14400 => \"candle4H\",\n            43200 => \"candle12H\",\n            86400 => \"candle1D\",\n            604800 => \"candle1W\",\n            _ => panic!(\"Invalid Bitget candlestick interval {interval}\"),\n        }\n    }\n}\n\nimpl MessageHandler for BitgetMessageHandler {\n    // the logic is almost the same with OKX\n    fn handle_message(&mut self, msg: &str) -> MiscMessage {\n        if msg == \"pong\" {\n            // see https://bitgetlimited.github.io/apidoc/en/spot/#connect\n            return MiscMessage::Pong;\n        }\n        let resp = serde_json::from_str::<HashMap<String, Value>>(msg);\n        if resp.is_err() {\n            error!(\"{} is not a JSON string, {}\", msg, EXCHANGE_NAME);\n            return MiscMessage::Other;\n        }\n        let obj = resp.unwrap();\n\n        if let Some(event) = obj.get(\"event\") {\n            match event.as_str().unwrap() {\n                \"error\" => error!(\"Received {} from {}\", msg, EXCHANGE_NAME),\n                \"subscribe\" => info!(\"Received {} from {}\", msg, EXCHANGE_NAME),\n                \"unsubscribe\" => info!(\"Received {} from {}\", msg, EXCHANGE_NAME),\n                _ => warn!(\"Received {} from {}\", msg, EXCHANGE_NAME),\n            }\n            MiscMessage::Other\n        } else if !obj.contains_key(\"arg\") || !obj.contains_key(\"data\") {\n            error!(\"Received {} from {}\", msg, EXCHANGE_NAME);\n            MiscMessage::Other\n        } else {\n            MiscMessage::Normal\n        }\n    }\n\n    fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)> {\n        // https://bitgetlimited.github.io/apidoc/en/spot/#connect\n        Some((Message::Text(\"ping\".to_string()), 30))\n    }\n}\n\nimpl<const MARKET_TYPE: char> CommandTranslator for BitgetCommandTranslator<MARKET_TYPE> {\n    fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec<String> {\n        ensure_frame_size(topics, subscribe, Self::topics_to_command, WS_FRAME_SIZE, None)\n    }\n\n    fn translate_to_candlestick_commands(\n        &self,\n        subscribe: bool,\n        symbol_interval_list: &[(String, usize)],\n    ) -> Vec<String> {\n        let topics = symbol_interval_list\n            .iter()\n            .map(|(symbol, interval)| {\n                let channel = Self::to_candlestick_raw_channel(*interval);\n                (channel.to_string(), symbol.to_string())\n            })\n            .collect::<Vec<(String, String)>>();\n        self.translate_to_commands(subscribe, &topics)\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use crate::common::command_translator::CommandTranslator;\n\n    #[test]\n    fn test_one_topic() {\n        let translator = super::BitgetCommandTranslator::<'S'> {};\n        let commands =\n            translator.translate_to_commands(true, &[(\"trade\".to_string(), \"BTCUSDT\".to_string())]);\n\n        assert_eq!(1, commands.len());\n        assert_eq!(\n            r#\"{\"op\":\"subscribe\",\"args\":[{\"channel\":\"trade\",\"instId\":\"BTCUSDT\",\"instType\":\"SP\"}]}\"#,\n            commands[0]\n        );\n    }\n\n    #[test]\n    fn test_two_topics() {\n        let translator = super::BitgetCommandTranslator::<'S'> {};\n        let commands = translator.translate_to_commands(\n            true,\n            &[\n                (\"trade\".to_string(), \"BTCUSDT\".to_string()),\n                (\"books\".to_string(), \"ETHUSDT\".to_string()),\n            ],\n        );\n\n        assert_eq!(1, commands.len());\n        assert_eq!(\n            r#\"{\"op\":\"subscribe\",\"args\":[{\"channel\":\"trade\",\"instId\":\"BTCUSDT\",\"instType\":\"SP\"},{\"channel\":\"books\",\"instId\":\"ETHUSDT\",\"instType\":\"SP\"}]}\"#,\n            commands[0]\n        );\n    }\n\n    #[test]\n    fn test_candlestick() {\n        let translator = super::BitgetCommandTranslator::<'S'> {};\n        let commands = translator.translate_to_candlestick_commands(\n            true,\n            &[(\"BTCUSDT\".to_string(), 60), (\"ETHUSDT\".to_string(), 300)],\n        );\n\n        assert_eq!(1, commands.len());\n        assert_eq!(\n            r#\"{\"op\":\"subscribe\",\"args\":[{\"channel\":\"candle1m\",\"instId\":\"BTCUSDT\",\"instType\":\"SP\"},{\"channel\":\"candle5m\",\"instId\":\"ETHUSDT\",\"instType\":\"SP\"}]}\"#,\n            commands[0]\n        );\n    }\n}\n"
  },
  {
    "path": "crypto-ws-client/src/clients/bithumb.rs",
    "content": "use async_trait::async_trait;\nuse std::collections::HashMap;\nuse tokio_tungstenite::tungstenite::Message;\n\nuse crate::{\n    clients::common_traits::{\n        Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO,\n    },\n    common::{\n        command_translator::CommandTranslator,\n        message_handler::{MessageHandler, MiscMessage},\n        ws_client_internal::WSClientInternal,\n    },\n    WSClient,\n};\n\nuse log::*;\nuse serde_json::Value;\n\npub(super) const EXCHANGE_NAME: &str = \"bithumb\";\n\nconst WEBSOCKET_URL: &str = \"wss://global-api.bithumb.pro/message/realtime\";\n\n/// The WebSocket client for Bithumb.\n///\n/// Bithumb has only Spot market.\n///\n///   * WebSocket API doc: <https://github.com/bithumb-pro/bithumb.pro-official-api-docs/blob/master/ws-api.md>\n///   * Trading at: <https://en.bithumb.com/trade/order/BTC_KRW>\npub struct BithumbWSClient {\n    client: WSClientInternal<BithumbMessageHandler>,\n    translator: BithumbCommandTranslator,\n}\n\nimpl_new_constructor!(\n    BithumbWSClient,\n    EXCHANGE_NAME,\n    WEBSOCKET_URL,\n    BithumbMessageHandler {},\n    BithumbCommandTranslator {}\n);\n\n#[rustfmt::skip]\nimpl_trait!(Trade, BithumbWSClient, subscribe_trade, \"TRADE\");\n#[rustfmt::skip]\nimpl_trait!(Ticker, BithumbWSClient, subscribe_ticker, \"TICKER\");\n#[rustfmt::skip]\nimpl_trait!(OrderBook, BithumbWSClient, subscribe_orderbook, \"ORDERBOOK\");\n\npanic_bbo!(BithumbWSClient);\npanic_candlestick!(BithumbWSClient);\npanic_l2_topk!(BithumbWSClient);\npanic_l3_orderbook!(BithumbWSClient);\n\nimpl_ws_client_trait!(BithumbWSClient);\n\nstruct BithumbMessageHandler {}\nstruct BithumbCommandTranslator {}\n\nimpl MessageHandler for BithumbMessageHandler {\n    fn handle_message(&mut self, msg: &str) -> MiscMessage {\n        let obj = serde_json::from_str::<HashMap<String, Value>>(msg).unwrap();\n        let code = obj.get(\"code\").unwrap().as_str().unwrap();\n        let code = code.parse::<i64>().unwrap();\n        if code < 10000 {\n            match code {\n                0 => MiscMessage::Pong,\n                6 => {\n                    let arr = obj.get(\"data\").unwrap().as_array();\n                    if arr.is_some() && arr.unwrap().is_empty() {\n                        // ignore empty data\n                        MiscMessage::Other\n                    } else {\n                        MiscMessage::Normal\n                    }\n                }\n                7 => MiscMessage::Normal,\n                _ => {\n                    debug!(\"Received {} from {}\", msg, EXCHANGE_NAME);\n                    MiscMessage::Other\n                }\n            }\n        } else {\n            panic!(\"Received {msg} from {EXCHANGE_NAME}\");\n        }\n    }\n\n    fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)> {\n        Some((Message::Text(r#\"{\"cmd\":\"ping\"}\"#.to_string()), 60))\n    }\n}\n\nimpl BithumbCommandTranslator {\n    fn topics_to_command(topics: &[(String, String)], subscribe: bool) -> String {\n        let raw_channels: Vec<String> =\n            topics.iter().map(|(channel, symbol)| format!(\"{channel}:{symbol}\")).collect();\n        format!(\n            r#\"{{\"cmd\":\"{}\",\"args\":{}}}\"#,\n            if subscribe { \"subscribe\" } else { \"unsubscribe\" },\n            serde_json::to_string(&raw_channels).unwrap()\n        )\n    }\n}\n\nimpl CommandTranslator for BithumbCommandTranslator {\n    fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec<String> {\n        vec![Self::topics_to_command(topics, subscribe)]\n    }\n\n    fn translate_to_candlestick_commands(\n        &self,\n        _subscribe: bool,\n        _symbol_interval_list: &[(String, usize)],\n    ) -> Vec<String> {\n        panic!(\"Bithumb does NOT have candlestick channel\");\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use crate::common::command_translator::CommandTranslator;\n\n    #[test]\n    fn test_two_symbols() {\n        let translator = super::BithumbCommandTranslator {};\n        let commands = translator.translate_to_commands(\n            true,\n            &[\n                (\"TRADE\".to_string(), \"BTC-USDT\".to_string()),\n                (\"TRADE\".to_string(), \"ETH-USDT\".to_string()),\n            ],\n        );\n\n        assert_eq!(1, commands.len());\n        assert_eq!(\n            r#\"{\"cmd\":\"subscribe\",\"args\":[\"TRADE:BTC-USDT\",\"TRADE:ETH-USDT\"]}\"#,\n            commands[0]\n        );\n    }\n\n    #[test]\n    fn test_two_channels() {\n        let translator = super::BithumbCommandTranslator {};\n        let commands = translator.translate_to_commands(\n            true,\n            &[\n                (\"TRADE\".to_string(), \"BTC-USDT\".to_string()),\n                (\"ORDERBOOK\".to_string(), \"BTC-USDT\".to_string()),\n            ],\n        );\n\n        assert_eq!(1, commands.len());\n        assert_eq!(\n            r#\"{\"cmd\":\"subscribe\",\"args\":[\"TRADE:BTC-USDT\",\"ORDERBOOK:BTC-USDT\"]}\"#,\n            commands[0]\n        );\n    }\n}\n"
  },
  {
    "path": "crypto-ws-client/src/clients/bitmex.rs",
    "content": "use async_trait::async_trait;\nuse std::{collections::HashMap, time::Duration};\nuse tokio_tungstenite::tungstenite::Message;\n\nuse crate::{\n    clients::common_traits::{\n        Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO,\n    },\n    common::{\n        command_translator::CommandTranslator,\n        message_handler::{MessageHandler, MiscMessage},\n        ws_client_internal::WSClientInternal,\n    },\n    WSClient,\n};\nuse log::*;\nuse serde_json::Value;\n\npub(super) const EXCHANGE_NAME: &str = \"bitmex\";\n\nconst WEBSOCKET_URL: &str = \"wss://www.bitmex.com/realtime\";\n\n// Too many args sent. Max length is 20\nconst MAX_CHANNELS_PER_COMMAND: usize = 20;\n\n/// The WebSocket client for BitMEX.\n///\n/// BitMEX has Swap and Future markets.\n///\n///   * WebSocket API doc: <https://www.bitmex.com/app/wsAPI>\n///   * Trading at: <https://www.bitmex.com/app/trade/>\npub struct BitmexWSClient {\n    client: WSClientInternal<BitmexMessageHandler>,\n    translator: BitmexCommandTranslator,\n}\n\nimpl_new_constructor!(\n    BitmexWSClient,\n    EXCHANGE_NAME,\n    WEBSOCKET_URL,\n    BitmexMessageHandler {},\n    BitmexCommandTranslator {}\n);\n\nimpl_trait!(Trade, BitmexWSClient, subscribe_trade, \"trade\");\nimpl_trait!(BBO, BitmexWSClient, subscribe_bbo, \"quote\");\n#[rustfmt::skip]\nimpl_trait!(OrderBook, BitmexWSClient, subscribe_orderbook, \"orderBookL2\");\n#[rustfmt::skip]\nimpl_trait!(OrderBookTopK, BitmexWSClient, subscribe_orderbook_topk, \"orderBook10\");\nimpl_candlestick!(BitmexWSClient);\npanic_l3_orderbook!(BitmexWSClient);\npanic_ticker!(BitmexWSClient);\n\nimpl_ws_client_trait!(BitmexWSClient);\n\nstruct BitmexMessageHandler {}\nstruct BitmexCommandTranslator {}\n\nimpl BitmexCommandTranslator {\n    fn topics_to_command(topics: &[(String, String)], subscribe: bool) -> String {\n        let raw_channels = topics\n            .iter()\n            .map(|(channel, symbol)| format!(\"{channel}:{symbol}\"))\n            .collect::<Vec<String>>();\n        format!(\n            r#\"{{\"op\":\"{}\",\"args\":{}}}\"#,\n            if subscribe { \"subscribe\" } else { \"unsubscribe\" },\n            serde_json::to_string(&raw_channels).unwrap()\n        )\n    }\n\n    // see https://www.okx.com/docs-v5/en/#websocket-api-public-channel-candlesticks-channel\n    fn to_candlestick_raw_channel(interval: usize) -> String {\n        let interval_str = match interval {\n            60 => \"1m\",\n            300 => \"5m\",\n            3600 => \"1h\",\n            86400 => \"1d\",\n            _ => panic!(\"BitMEX has intervals 1m,5m,1h,1d\"),\n        };\n        format!(\"tradeBin{interval_str}\")\n    }\n}\n\nimpl MessageHandler for BitmexMessageHandler {\n    fn handle_message(&mut self, msg: &str) -> MiscMessage {\n        if msg == \"pong\" {\n            return MiscMessage::Pong;\n        }\n        let resp = serde_json::from_str::<HashMap<String, Value>>(msg);\n        if resp.is_err() {\n            error!(\"{} is not a JSON string, {}\", msg, EXCHANGE_NAME);\n            return MiscMessage::Other;\n        }\n        let obj = resp.unwrap();\n\n        if obj.contains_key(\"error\") {\n            let error_msg = obj.get(\"error\").unwrap().as_str().unwrap();\n            let code = obj.get(\"status\").unwrap().as_i64().unwrap();\n\n            match code {\n                // Rate limit exceeded\n                429 => {\n                    error!(\"Received {} from {}\", msg, EXCHANGE_NAME);\n                    std::thread::sleep(Duration::from_secs(3));\n                }\n                400 => {\n                    if error_msg.starts_with(\"Unknown\") {\n                        panic!(\"Received {msg} from {EXCHANGE_NAME}\");\n                    } else if error_msg.starts_with(\"You are already subscribed to this topic\") {\n                        info!(\"Received {} from {}\", msg, EXCHANGE_NAME)\n                    } else {\n                        warn!(\"Received {} from {}\", msg, EXCHANGE_NAME);\n                    }\n                }\n                _ => error!(\"Received {} from {}\", msg, EXCHANGE_NAME),\n            }\n            MiscMessage::Other\n        } else if obj.contains_key(\"success\") || obj.contains_key(\"info\") {\n            info!(\"Received {} from {}\", msg, EXCHANGE_NAME);\n            MiscMessage::Other\n        } else if obj.contains_key(\"table\")\n            && obj.contains_key(\"action\")\n            && obj.contains_key(\"data\")\n        {\n            MiscMessage::Normal\n        } else {\n            warn!(\"Received {} from {}\", msg, EXCHANGE_NAME);\n            MiscMessage::Other\n        }\n    }\n\n    fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)> {\n        Some((Message::Text(\"ping\".to_string()), 5))\n    }\n}\n\nimpl CommandTranslator for BitmexCommandTranslator {\n    fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec<String> {\n        let mut commands: Vec<String> = Vec::new();\n\n        let n = topics.len();\n        for i in (0..n).step_by(MAX_CHANNELS_PER_COMMAND) {\n            let chunk: Vec<(String, String)> =\n                (topics[i..(std::cmp::min(i + MAX_CHANNELS_PER_COMMAND, n))]).to_vec();\n            commands.push(Self::topics_to_command(&chunk, subscribe));\n        }\n\n        commands\n    }\n\n    fn translate_to_candlestick_commands(\n        &self,\n        subscribe: bool,\n        symbol_interval_list: &[(String, usize)],\n    ) -> Vec<String> {\n        let topics = symbol_interval_list\n            .iter()\n            .map(|(symbol, interval)| {\n                let channel = Self::to_candlestick_raw_channel(*interval);\n                (channel, symbol.to_string())\n            })\n            .collect::<Vec<(String, String)>>();\n        self.translate_to_commands(subscribe, &topics)\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use crate::common::command_translator::CommandTranslator;\n\n    #[test]\n    fn test_one_topic() {\n        let translator = super::BitmexCommandTranslator {};\n        let commands =\n            translator.translate_to_commands(true, &[(\"trade\".to_string(), \"XBTUSD\".to_string())]);\n\n        assert_eq!(1, commands.len());\n        assert_eq!(r#\"{\"op\":\"subscribe\",\"args\":[\"trade:XBTUSD\"]}\"#, commands[0]);\n    }\n\n    #[test]\n    fn test_multiple_topics() {\n        let translator = super::BitmexCommandTranslator {};\n        let commands = translator.translate_to_commands(\n            true,\n            &[\n                (\"trade\".to_string(), \"XBTUSD\".to_string()),\n                (\"quote\".to_string(), \"XBTUSD\".to_string()),\n                (\"orderBookL2_25\".to_string(), \"XBTUSD\".to_string()),\n                (\"tradeBin1m\".to_string(), \"XBTUSD\".to_string()),\n            ],\n        );\n\n        assert_eq!(1, commands.len());\n        assert_eq!(\n            r#\"{\"op\":\"subscribe\",\"args\":[\"trade:XBTUSD\",\"quote:XBTUSD\",\"orderBookL2_25:XBTUSD\",\"tradeBin1m:XBTUSD\"]}\"#,\n            commands[0]\n        );\n    }\n}\n"
  },
  {
    "path": "crypto-ws-client/src/clients/bitstamp.rs",
    "content": "use async_trait::async_trait;\nuse std::collections::HashMap;\nuse tokio_tungstenite::tungstenite::Message;\n\nuse crate::{\n    clients::common_traits::{\n        Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO,\n    },\n    common::{\n        command_translator::CommandTranslator,\n        message_handler::{MessageHandler, MiscMessage},\n        ws_client_internal::WSClientInternal,\n    },\n    WSClient,\n};\nuse log::*;\nuse serde_json::Value;\n\npub(super) const EXCHANGE_NAME: &str = \"bitstamp\";\n\nconst WEBSOCKET_URL: &str = \"wss://ws.bitstamp.net\";\n\n/// The WebSocket client for Bitstamp Spot market.\n///\n/// Bitstamp has only Spot market.\n///\n///   * WebSocket API doc: <https://www.bitstamp.net/websocket/v2/>\n///   * Trading at: <https://www.bitstamp.net/market/tradeview/>\npub struct BitstampWSClient {\n    client: WSClientInternal<BitstampMessageHandler>,\n    translator: BitstampCommandTranslator,\n}\n\nimpl_new_constructor!(\n    BitstampWSClient,\n    EXCHANGE_NAME,\n    WEBSOCKET_URL,\n    BitstampMessageHandler {},\n    BitstampCommandTranslator {}\n);\n\nimpl_trait!(Trade, BitstampWSClient, subscribe_trade, \"live_trades\");\n#[rustfmt::skip]\nimpl_trait!(OrderBook, BitstampWSClient, subscribe_orderbook, \"diff_order_book\");\n#[rustfmt::skip]\nimpl_trait!(OrderBookTopK, BitstampWSClient, subscribe_orderbook_topk, \"order_book\");\n#[rustfmt::skip]\nimpl_trait!(Level3OrderBook, BitstampWSClient, subscribe_l3_orderbook, \"live_orders\");\n\npanic_bbo!(BitstampWSClient);\nimpl_candlestick!(BitstampWSClient);\npanic_ticker!(BitstampWSClient);\n\nimpl_ws_client_trait!(BitstampWSClient);\n\nstruct BitstampMessageHandler {}\nstruct BitstampCommandTranslator {}\n\nimpl MessageHandler for BitstampMessageHandler {\n    fn handle_message(&mut self, msg: &str) -> MiscMessage {\n        let resp = serde_json::from_str::<HashMap<String, Value>>(msg);\n        if resp.is_err() {\n            error!(\"{} is not a JSON string, {}\", msg, EXCHANGE_NAME);\n            return MiscMessage::Other;\n        }\n        let obj = resp.unwrap();\n\n        let event = obj.get(\"event\").unwrap().as_str().unwrap();\n        match event {\n            \"bts:subscription_succeeded\" | \"bts:unsubscription_succeeded\" | \"bts:heartbeat\" => {\n                debug!(\"Received {} from {}\", msg, EXCHANGE_NAME);\n                MiscMessage::Other\n            }\n            \"bts:error\" => {\n                error!(\"Received {} from {}\", msg, EXCHANGE_NAME);\n                panic!(\"Received {msg} from {EXCHANGE_NAME}\");\n            }\n            \"bts:request_reconnect\" => {\n                warn!(\"Received {}, which means Bitstamp is under maintenance\", msg);\n                std::thread::sleep(std::time::Duration::from_secs(20));\n                MiscMessage::Reconnect\n            }\n            _ => MiscMessage::Normal,\n        }\n    }\n\n    fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)> {\n        // See \"Heartbeat\" at https://www.bitstamp.net/websocket/v2/\n        Some((Message::Text(r#\"{\"event\": \"bts:heartbeat\"}\"#.to_string()), 10))\n    }\n}\n\nimpl CommandTranslator for BitstampCommandTranslator {\n    fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec<String> {\n        topics\n            .iter()\n            .map(|(channel, symbol)| {\n                format!(\n                    r#\"{{\"event\":\"bts:{}\",\"data\":{{\"channel\":\"{}_{}\"}}}}\"#,\n                    if subscribe { \"subscribe\" } else { \"unsubscribe\" },\n                    channel,\n                    symbol,\n                )\n            })\n            .collect()\n    }\n\n    fn translate_to_candlestick_commands(\n        &self,\n        _subscribe: bool,\n        _symbol_interval_list: &[(String, usize)],\n    ) -> Vec<String> {\n        panic!(\"Bitstamp does NOT have candlestick channel\");\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use crate::common::command_translator::CommandTranslator;\n\n    #[test]\n    fn test_one_topic() {\n        let translator = super::BitstampCommandTranslator {};\n        let commands = translator\n            .translate_to_commands(true, &[(\"live_trades\".to_string(), \"btcusd\".to_string())]);\n\n        assert_eq!(1, commands.len());\n        assert_eq!(\n            r#\"{\"event\":\"bts:subscribe\",\"data\":{\"channel\":\"live_trades_btcusd\"}}\"#,\n            commands[0]\n        );\n    }\n\n    #[test]\n    fn test_two_topics() {\n        let translator = super::BitstampCommandTranslator {};\n        let commands = translator.translate_to_commands(\n            true,\n            &[\n                (\"live_trades\".to_string(), \"btcusd\".to_string()),\n                (\"diff_order_book\".to_string(), \"btcusd\".to_string()),\n            ],\n        );\n\n        assert_eq!(2, commands.len());\n        assert_eq!(\n            r#\"{\"event\":\"bts:subscribe\",\"data\":{\"channel\":\"live_trades_btcusd\"}}\"#,\n            commands[0]\n        );\n        assert_eq!(\n            r#\"{\"event\":\"bts:subscribe\",\"data\":{\"channel\":\"diff_order_book_btcusd\"}}\"#,\n            commands[1]\n        );\n    }\n}\n"
  },
  {
    "path": "crypto-ws-client/src/clients/bitz/bitz_spot.rs",
    "content": "use async_trait::async_trait;\nuse std::{\n    collections::HashMap,\n    time::{SystemTime, UNIX_EPOCH},\n};\nuse tokio_tungstenite::tungstenite::Message;\n\nuse crate::{\n    clients::common_traits::{\n        Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO,\n    },\n    common::{\n        command_translator::CommandTranslator,\n        message_handler::{MessageHandler, MiscMessage},\n        ws_client_internal::WSClientInternal,\n    },\n    WSClient,\n};\n\nuse log::*;\nuse serde_json::Value;\n\nuse super::EXCHANGE_NAME;\n\nconst WEBSOCKET_URL: &str = \"wss://wsapi.bitz.plus/\";\n\n/// The WebSocket client for Bitz spot market.\n///\n/// * WebSocket API doc: <https://apidocv2.bitz.plus/en/#websocket-url>\n/// * Trading at <https://www.bitz.plus/exchange>\npub struct BitzSpotWSClient {\n    client: WSClientInternal<BitzMessageHandler>,\n    translator: BitzCommandTranslator,\n}\n\nimpl_new_constructor!(\n    BitzSpotWSClient,\n    EXCHANGE_NAME,\n    WEBSOCKET_URL,\n    BitzMessageHandler {},\n    BitzCommandTranslator {}\n);\n\n#[rustfmt::skip]\nimpl_trait!(Trade, BitzSpotWSClient, subscribe_trade, \"order\");\n#[rustfmt::skip]\nimpl_trait!(OrderBook, BitzSpotWSClient, subscribe_orderbook, \"depth\");\n#[rustfmt::skip]\nimpl_trait!(Ticker, BitzSpotWSClient, subscribe_ticker, \"market\");\nimpl_candlestick!(BitzSpotWSClient);\n\npanic_bbo!(BitzSpotWSClient);\npanic_l2_topk!(BitzSpotWSClient);\npanic_l3_orderbook!(BitzSpotWSClient);\n\nimpl_ws_client_trait!(BitzSpotWSClient);\n\nstruct BitzMessageHandler {}\nstruct BitzCommandTranslator {}\n\nimpl MessageHandler for BitzMessageHandler {\n    fn handle_message(&mut self, msg: &str) -> MiscMessage {\n        if msg == \"pong\" {\n            return MiscMessage::Pong;\n        }\n        let obj = serde_json::from_str::<HashMap<String, Value>>(msg).unwrap();\n\n        if obj.contains_key(\"action\")\n            && obj.get(\"action\").unwrap().as_str().unwrap().starts_with(\"Pushdata.\")\n        {\n            MiscMessage::Normal\n        } else if obj.contains_key(\"status\") {\n            let status = obj.get(\"status\").unwrap().as_i64().unwrap();\n            // see https://apidocv2.bitz.plus/en/#error\n            match status {\n                -101001 => {\n                    error!(\"Subscription type parameter error: {}\", msg);\n                    panic!(\"Subscription type parameter error: {msg}\");\n                }\n                -101002 => {\n                    error!(\"Fail to get subscribed symbol of trading pair: {}\", msg);\n                    panic!(\"Fail to get subscribed symbol of trading pair: {msg}\");\n                }\n                -101003 => {\n                    error!(\"k-line scale resolution error: {}\", msg);\n                    panic!(\"k-line scale resolution error: {msg}\");\n                }\n                _ => warn!(\"Received {} from {}\", msg, EXCHANGE_NAME),\n            }\n            MiscMessage::Other\n        } else {\n            warn!(\"Received {} from {}\", msg, EXCHANGE_NAME);\n            MiscMessage::Other\n        }\n    }\n\n    fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)> {\n        // See https://apidocv2.bitz.plus/en/#heartbeat-and-persistent-connection-strategy\n        Some((Message::Text(\"ping\".to_string()), 10))\n    }\n}\n\nimpl BitzCommandTranslator {\n    fn symbol_channels_to_command(pair: &str, channels: &[String], subscribe: bool) -> String {\n        format!(\n            r#\"{{\"action\":\"Topic.{}\", \"data\":{{\"symbol\":\"{}\", \"type\":\"{}\", \"_CDID\":\"100002\", \"dataType\":\"1\"}}, \"msg_id\":{}}}\"#,\n            if subscribe { \"sub\" } else { \"unsub\" },\n            pair,\n            channels.join(\",\"),\n            SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis(),\n        )\n    }\n\n    fn to_candlestick_command(symbol: &str, interval: usize, subscribe: bool) -> String {\n        let interval_str = match interval {\n            60 => \"1min\",\n            300 => \"5min\",\n            900 => \"15min\",\n            1800 => \"30min\",\n            3600 => \"60min\",\n            14400 => \"4hour\",\n            86400 => \"1day\",\n            432000 => \"5day\",\n            604800 => \"1week\",\n            2592000 => \"1mon\",\n            _ => panic!(\n                \"Bitz available intervals 1min,5min,15min,30min,60min,4hour,1day,5day,1week,1mon\"\n            ),\n        };\n        format!(\n            r#\"{{\"action\":\"Topic.{}\", \"data\":{{\"symbol\":\"{}\", \"type\":\"kline\", \"resolution\":\"{}\", \"_CDID\":\"100002\", \"dataType\":\"1\"}}, \"msg_id\":{}}}\"#,\n            if subscribe { \"sub\" } else { \"unsub\" },\n            symbol,\n            interval_str,\n            SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis(),\n        )\n    }\n}\n\nimpl CommandTranslator for BitzCommandTranslator {\n    fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec<String> {\n        let mut commands: Vec<String> = Vec::new();\n\n        let mut symbol_channels = HashMap::<String, Vec<String>>::new();\n        for (channel, symbol) in topics {\n            match symbol_channels.get_mut(symbol) {\n                Some(channels) => channels.push(channel.to_string()),\n                None => {\n                    symbol_channels.insert(symbol.to_string(), vec![channel.to_string()]);\n                }\n            }\n        }\n\n        for (symbol, channels) in symbol_channels.iter() {\n            commands.push(Self::symbol_channels_to_command(symbol, channels, subscribe));\n        }\n\n        commands\n    }\n\n    fn translate_to_candlestick_commands(\n        &self,\n        subscribe: bool,\n        symbol_interval_list: &[(String, usize)],\n    ) -> Vec<String> {\n        symbol_interval_list\n            .iter()\n            .map(|(symbol, interval)| Self::to_candlestick_command(symbol, *interval, subscribe))\n            .collect::<Vec<String>>()\n    }\n}\n"
  },
  {
    "path": "crypto-ws-client/src/clients/bitz/mod.rs",
    "content": "mod bitz_spot;\n// mod bitz_swap;\n\npub use bitz_spot::BitzSpotWSClient;\n// pub use bitz_swap::BitzSwapWSClient;\n\npub(super) const EXCHANGE_NAME: &str = \"bitz\";\n"
  },
  {
    "path": "crypto-ws-client/src/clients/bybit/bybit_inverse.rs",
    "content": "use async_trait::async_trait;\n\nuse crate::{\n    clients::common_traits::{\n        Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO,\n    },\n    common::{command_translator::CommandTranslator, ws_client_internal::WSClientInternal},\n    WSClient,\n};\n\nuse super::utils::{BybitMessageHandler, EXCHANGE_NAME};\n\nconst WEBSOCKET_URL: &str = \"wss://stream.bybit.com/realtime\";\n\n/// Bybit Inverses markets.\n///\n/// InverseFuture:\n///   * WebSocket API doc: <https://bybit-exchange.github.io/docs/inverse_futures/>\n///   * Trading at: <https://www.bybit.com/trade/inverse/futures/BTCUSD_BIQ>\n///\n/// InverseSwap:\n///   * WebSocket API doc: <https://bybit-exchange.github.io/docs/inverse/#t-websocket>\n///   * Trading at: <https://www.bybit.com/trade/inverse/>\npub struct BybitInverseWSClient {\n    client: WSClientInternal<BybitMessageHandler>,\n    translator: BybitInverseCommandTranslator,\n}\n\nimpl_new_constructor!(\n    BybitInverseWSClient,\n    EXCHANGE_NAME,\n    WEBSOCKET_URL,\n    BybitMessageHandler {},\n    BybitInverseCommandTranslator {}\n);\n\nimpl_trait!(Trade, BybitInverseWSClient, subscribe_trade, \"trade\");\n#[rustfmt::skip]\n// Prefer orderBookL2_25 over orderBook_200.100ms because /public/orderBook/L2\n// returns a top 25 snapshot, which is the same depth as orderBookL2_25.\nimpl_trait!(OrderBook, BybitInverseWSClient, subscribe_orderbook, \"orderBookL2_25\");\n#[rustfmt::skip]\nimpl_trait!(Ticker, BybitInverseWSClient, subscribe_ticker, \"instrument_info.100ms\");\nimpl_candlestick!(BybitInverseWSClient);\npanic_bbo!(BybitInverseWSClient);\npanic_l3_orderbook!(BybitInverseWSClient);\npanic_l2_topk!(BybitInverseWSClient);\n\nimpl_ws_client_trait!(BybitInverseWSClient);\n\nstruct BybitInverseCommandTranslator {}\n\nimpl BybitInverseCommandTranslator {\n    // https://bybit-exchange.github.io/docs/inverse_futures/#t-websocketklinev2\n    // https://bybit-exchange.github.io/docs/inverse/#t-websocketklinev2\n    fn to_candlestick_raw_channel(interval: usize) -> String {\n        let interval_str = match interval {\n            60 => \"1\",\n            180 => \"3\",\n            300 => \"5\",\n            900 => \"15\",\n            1800 => \"30\",\n            3600 => \"60\",\n            7200 => \"120\",\n            14400 => \"240\",\n            21600 => \"360\",\n            86400 => \"D\",\n            604800 => \"W\",\n            2592000 => \"M\",\n            _ => panic!(\n                \"Bybit InverseFuture has intervals 1min,5min,15min,30min,60min,4hour,1day,1week,1mon\"\n            ),\n        };\n        format!(\"klineV2.{interval_str}\")\n    }\n}\n\nimpl CommandTranslator for BybitInverseCommandTranslator {\n    fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec<String> {\n        vec![super::utils::topics_to_command(topics, subscribe)]\n    }\n\n    fn translate_to_candlestick_commands(\n        &self,\n        subscribe: bool,\n        symbol_interval_list: &[(String, usize)],\n    ) -> Vec<String> {\n        let topics = symbol_interval_list\n            .iter()\n            .map(|(symbol, interval)| {\n                let channel = Self::to_candlestick_raw_channel(*interval);\n                (channel, symbol.to_string())\n            })\n            .collect::<Vec<(String, String)>>();\n        self.translate_to_commands(subscribe, &topics)\n    }\n}\n"
  },
  {
    "path": "crypto-ws-client/src/clients/bybit/bybit_linear_swap.rs",
    "content": "use async_trait::async_trait;\n\nuse crate::{\n    clients::common_traits::{\n        Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO,\n    },\n    common::{command_translator::CommandTranslator, ws_client_internal::WSClientInternal},\n    WSClient,\n};\n\nuse super::utils::{BybitMessageHandler, EXCHANGE_NAME};\n\nconst WEBSOCKET_URL: &str = \"wss://stream.bybit.com/realtime_public\";\n\n/// Bybit LinearSwap market.\n///\n/// * WebSocket API doc: <https://bybit-exchange.github.io/docs/inverse/#t-websocket>\n/// * Trading at: <https://www.bybit.com/trade/inverse/>\npub struct BybitLinearSwapWSClient {\n    client: WSClientInternal<BybitMessageHandler>,\n    translator: BybitLinearCommandTranslator,\n}\n\nimpl_new_constructor!(\n    BybitLinearSwapWSClient,\n    EXCHANGE_NAME,\n    WEBSOCKET_URL,\n    BybitMessageHandler {},\n    BybitLinearCommandTranslator {}\n);\n\nimpl_trait!(Trade, BybitLinearSwapWSClient, subscribe_trade, \"trade\");\n#[rustfmt::skip]\n// Prefer orderBookL2_25 over orderBook_200.100ms because /public/orderBook/L2\n// returns a top 25 snapshot, which is the same depth as orderBookL2_25.\nimpl_trait!(OrderBook, BybitLinearSwapWSClient, subscribe_orderbook, \"orderBookL2_25\");\n#[rustfmt::skip]\nimpl_trait!(Ticker, BybitLinearSwapWSClient, subscribe_ticker, \"instrument_info.100ms\");\nimpl_candlestick!(BybitLinearSwapWSClient);\npanic_bbo!(BybitLinearSwapWSClient);\npanic_l3_orderbook!(BybitLinearSwapWSClient);\npanic_l2_topk!(BybitLinearSwapWSClient);\n\nimpl_ws_client_trait!(BybitLinearSwapWSClient);\n\nstruct BybitLinearCommandTranslator {}\n\nimpl BybitLinearCommandTranslator {\n    // https://bybit-exchange.github.io/docs/linear/#t-websocketkline\n    fn to_candlestick_raw_channel(interval: usize) -> String {\n        let interval_str = match interval {\n            60 => \"1\",\n            180 => \"3\",\n            300 => \"5\",\n            900 => \"15\",\n            1800 => \"30\",\n            3600 => \"60\",\n            7200 => \"120\",\n            14400 => \"240\",\n            21600 => \"360\",\n            86400 => \"D\",\n            604800 => \"W\",\n            2592000 => \"M\",\n            _ => panic!(\n                \"Bybit LinearSwap has intervals 1min,5min,15min,30min,60min,4hour,1day,1week,1mon\"\n            ),\n        };\n        format!(\"candle.{interval_str}\")\n    }\n}\n\nimpl CommandTranslator for BybitLinearCommandTranslator {\n    fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec<String> {\n        vec![super::utils::topics_to_command(topics, subscribe)]\n    }\n\n    fn translate_to_candlestick_commands(\n        &self,\n        subscribe: bool,\n        symbol_interval_list: &[(String, usize)],\n    ) -> Vec<String> {\n        let topics = symbol_interval_list\n            .iter()\n            .map(|(symbol, interval)| {\n                let channel = Self::to_candlestick_raw_channel(*interval);\n                (channel, symbol.to_string())\n            })\n            .collect::<Vec<(String, String)>>();\n        self.translate_to_commands(subscribe, &topics)\n    }\n}\n"
  },
  {
    "path": "crypto-ws-client/src/clients/bybit/mod.rs",
    "content": "mod bybit_inverse;\nmod bybit_linear_swap;\nmod utils;\n\npub use bybit_inverse::BybitInverseWSClient;\npub use bybit_linear_swap::BybitLinearSwapWSClient;\n"
  },
  {
    "path": "crypto-ws-client/src/clients/bybit/utils.rs",
    "content": "use std::collections::HashMap;\n\nuse log::*;\nuse serde_json::Value;\nuse tokio_tungstenite::tungstenite::Message;\n\nuse crate::common::message_handler::{MessageHandler, MiscMessage};\n\npub(super) const EXCHANGE_NAME: &str = \"bybit\";\n\npub(super) fn topics_to_command(topics: &[(String, String)], subscribe: bool) -> String {\n    let raw_channels = topics\n        .iter()\n        .map(|(channel, symbol)| format!(\"{channel}.{symbol}\"))\n        .collect::<Vec<String>>();\n    format!(\n        r#\"{{\"op\":\"{}\",\"args\":{}}}\"#,\n        if subscribe { \"subscribe\" } else { \"unsubscribe\" },\n        serde_json::to_string(&raw_channels).unwrap()\n    )\n}\n\npub(super) struct BybitMessageHandler {}\n\nimpl MessageHandler for BybitMessageHandler {\n    fn handle_message(&mut self, msg: &str) -> MiscMessage {\n        let obj = serde_json::from_str::<HashMap<String, Value>>(msg).unwrap();\n\n        if obj.contains_key(\"topic\") && obj.contains_key(\"data\") {\n            MiscMessage::Normal\n        } else {\n            if obj.contains_key(\"success\") {\n                if obj.get(\"success\").unwrap().as_bool().unwrap() {\n                    info!(\"Received {} from {}\", msg, EXCHANGE_NAME);\n                    if obj.contains_key(\"ret_msg\")\n                        && obj.get(\"ret_msg\").unwrap().as_str().unwrap() == \"pong\"\n                    {\n                        return MiscMessage::Pong;\n                    }\n                } else {\n                    error!(\"Received {} from {}\", msg, EXCHANGE_NAME);\n                    panic!(\"Received {msg} from {EXCHANGE_NAME}\");\n                }\n            } else {\n                warn!(\"Received {} from {}\", msg, EXCHANGE_NAME);\n            }\n            MiscMessage::Other\n        }\n    }\n\n    fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)> {\n        // See:\n        // - https://bybit-exchange.github.io/docs/inverse/#t-heartbeat\n        // - https://bybit-exchange.github.io/docs/linear/#t-heartbeat\n        Some((Message::Text(r#\"{\"op\":\"ping\"}\"#.to_string()), 30))\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    #[test]\n    fn test_one_channel() {\n        let command =\n            super::topics_to_command(&[(\"trade\".to_string(), \"BTCUSD\".to_string())], true);\n        assert_eq!(r#\"{\"op\":\"subscribe\",\"args\":[\"trade.BTCUSD\"]}\"#, command);\n    }\n\n    #[test]\n    fn test_multiple_channels() {\n        let command = super::topics_to_command(\n            &[\n                (\"trade\".to_string(), \"BTCUSD\".to_string()),\n                (\"orderBookL2_25\".to_string(), \"BTCUSD\".to_string()),\n                (\"instrument_info.100ms\".to_string(), \"BTCUSD\".to_string()),\n            ],\n            true,\n        );\n\n        assert_eq!(\n            r#\"{\"op\":\"subscribe\",\"args\":[\"trade.BTCUSD\",\"orderBookL2_25.BTCUSD\",\"instrument_info.100ms.BTCUSD\"]}\"#,\n            command\n        );\n    }\n}\n"
  },
  {
    "path": "crypto-ws-client/src/clients/coinbase_pro.rs",
    "content": "use async_trait::async_trait;\nuse std::collections::{BTreeMap, HashMap};\nuse tokio_tungstenite::tungstenite::Message;\n\nuse crate::{\n    clients::common_traits::{\n        Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO,\n    },\n    common::{\n        command_translator::CommandTranslator,\n        message_handler::{MessageHandler, MiscMessage},\n        ws_client_internal::WSClientInternal,\n    },\n    WSClient,\n};\nuse log::*;\nuse serde_json::Value;\n\npub(super) const EXCHANGE_NAME: &str = \"coinbase_pro\";\n\nconst WEBSOCKET_URL: &str = \"wss://ws-feed.exchange.coinbase.com\";\n\n/// The WebSocket client for CoinbasePro.\n///\n/// CoinbasePro has only Spot market.\n///\n///   * WebSocket API doc: <https://docs.cloud.coinbase.com/exchange/docs/websocket-overview>\n///   * Trading at: <https://pro.coinbase.com/>\npub struct CoinbaseProWSClient {\n    client: WSClientInternal<CoinbaseProMessageHandler>,\n    translator: CoinbaseProCommandTranslator,\n}\n\nimpl_new_constructor!(\n    CoinbaseProWSClient,\n    EXCHANGE_NAME,\n    WEBSOCKET_URL,\n    CoinbaseProMessageHandler {},\n    CoinbaseProCommandTranslator {}\n);\n\nimpl_trait!(Trade, CoinbaseProWSClient, subscribe_trade, \"matches\");\nimpl_trait!(Ticker, CoinbaseProWSClient, subscribe_ticker, \"ticker\");\n#[rustfmt::skip]\nimpl_trait!(OrderBook, CoinbaseProWSClient, subscribe_orderbook, \"level2\");\n#[rustfmt::skip]\nimpl_trait!(Level3OrderBook, CoinbaseProWSClient, subscribe_l3_orderbook, \"full\");\n\npanic_bbo!(CoinbaseProWSClient);\npanic_candlestick!(CoinbaseProWSClient);\npanic_l2_topk!(CoinbaseProWSClient);\n\nimpl_ws_client_trait!(CoinbaseProWSClient);\n\nstruct CoinbaseProMessageHandler {}\nstruct CoinbaseProCommandTranslator {}\n\nimpl MessageHandler for CoinbaseProMessageHandler {\n    fn handle_message(&mut self, msg: &str) -> MiscMessage {\n        let resp = serde_json::from_str::<HashMap<String, Value>>(msg);\n        if resp.is_err() {\n            error!(\"{} is not a JSON string, {}\", msg, EXCHANGE_NAME);\n            return MiscMessage::Other;\n        }\n        let obj = resp.unwrap();\n\n        match obj.get(\"type\").unwrap().as_str().unwrap() {\n            \"error\" => {\n                error!(\"Received {} from {}\", msg, EXCHANGE_NAME);\n                if obj.contains_key(\"reason\")\n                    && obj\n                        .get(\"reason\")\n                        .unwrap()\n                        .as_str()\n                        .unwrap()\n                        .contains(\"is not a valid product\")\n                {\n                    panic!(\"Received {msg} from {EXCHANGE_NAME}\");\n                } else {\n                    MiscMessage::Other\n                }\n            }\n            \"subscriptions\" => {\n                info!(\"Received {} from {}\", msg, EXCHANGE_NAME);\n                MiscMessage::Other\n            }\n            \"heartbeat\" => {\n                debug!(\"Received {} from {}\", msg, EXCHANGE_NAME);\n                MiscMessage::Other\n            }\n            _ => MiscMessage::Normal,\n        }\n    }\n\n    fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)> {\n        None\n    }\n}\n\nimpl CommandTranslator for CoinbaseProCommandTranslator {\n    fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec<String> {\n        let mut commands: Vec<String> = Vec::new();\n\n        let mut channel_symbols = BTreeMap::<String, Vec<String>>::new();\n        for (channel, symbol) in topics {\n            match channel_symbols.get_mut(channel) {\n                Some(symbols) => symbols.push(symbol.to_string()),\n                None => {\n                    channel_symbols.insert(channel.to_string(), vec![symbol.to_string()]);\n                }\n            }\n        }\n\n        if !channel_symbols.is_empty() {\n            let mut command = String::new();\n            command.push_str(\n                format!(\n                    r#\"{{\"type\":\"{}\",\"channels\": [\"#,\n                    if subscribe { \"subscribe\" } else { \"unsubscribe\" }\n                )\n                .as_str(),\n            );\n            for (channel, symbols) in channel_symbols.iter() {\n                command.push_str(\n                    format!(\n                        r#\"{{\"name\":\"{}\",\"product_ids\":{}}}\"#,\n                        channel,\n                        serde_json::to_string(symbols).unwrap(),\n                    )\n                    .as_str(),\n                );\n                command.push(',')\n            }\n            command.pop();\n            command.push_str(\"]}\");\n\n            commands.push(command);\n        }\n\n        commands\n    }\n\n    fn translate_to_candlestick_commands(\n        &self,\n        _subscribe: bool,\n        _symbol_interval_list: &[(String, usize)],\n    ) -> Vec<String> {\n        panic!(\"CoinbasePro does NOT have candlestick channel\");\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use crate::common::command_translator::CommandTranslator;\n\n    #[test]\n    fn test_two_symbols() {\n        let translator = super::CoinbaseProCommandTranslator {};\n        let commands = translator.translate_to_commands(\n            true,\n            &[\n                (\"matches\".to_string(), \"BTC-USD\".to_string()),\n                (\"matches\".to_string(), \"ETH-USD\".to_string()),\n            ],\n        );\n\n        assert_eq!(1, commands.len());\n        assert_eq!(\n            r#\"{\"type\":\"subscribe\",\"channels\": [{\"name\":\"matches\",\"product_ids\":[\"BTC-USD\",\"ETH-USD\"]}]}\"#,\n            commands[0]\n        );\n    }\n\n    #[test]\n    fn test_two_channels() {\n        let translator = super::CoinbaseProCommandTranslator {};\n        let commands = translator.translate_to_commands(\n            true,\n            &[\n                (\"matches\".to_string(), \"BTC-USD\".to_string()),\n                (\"level2\".to_string(), \"BTC-USD\".to_string()),\n            ],\n        );\n\n        assert_eq!(1, commands.len());\n        assert_eq!(\n            r#\"{\"type\":\"subscribe\",\"channels\": [{\"name\":\"level2\",\"product_ids\":[\"BTC-USD\"]},{\"name\":\"matches\",\"product_ids\":[\"BTC-USD\"]}]}\"#,\n            commands[0]\n        );\n    }\n}\n"
  },
  {
    "path": "crypto-ws-client/src/clients/common_traits.rs",
    "content": "use async_trait::async_trait;\n\n// tick-by-tick trade\n#[async_trait]\npub(super) trait Trade {\n    async fn subscribe_trade(&self, symbols: &[String]);\n}\n\n// 24hr rolling window ticker\n#[async_trait]\npub(super) trait Ticker {\n    async fn subscribe_ticker(&self, symbols: &[String]);\n}\n\n// Best Bid & Offer\n#[allow(clippy::upper_case_acronyms)]\n#[async_trait]\npub(super) trait BBO {\n    async fn subscribe_bbo(&self, symbols: &[String]);\n}\n\n// An orderbook snapshot followed by realtime updates.\n#[async_trait]\npub(super) trait OrderBook {\n    async fn subscribe_orderbook(&self, symbols: &[String]);\n}\n\n#[async_trait]\npub(super) trait OrderBookTopK {\n    /// Subscribes to level2 orderbook top-k snapshot channels.\n    async fn subscribe_orderbook_topk(&self, symbols: &[String]);\n}\n\n/// Level3 orderbook data.\n#[async_trait]\npub(super) trait Level3OrderBook {\n    /// Subscribes to level3 orderebook channels.\n    ///\n    /// The level3 orderbook is the orginal orderbook of an exchange, it is\n    /// non-aggregated by price level and updated tick-by-tick.\n    async fn subscribe_l3_orderbook(&self, symbols: &[String]);\n}\n\n#[async_trait]\npub(super) trait Candlestick {\n    /// Subscribes to candlestick channels which send OHLCV messages.\n    ///\n    /// `symbol_interval_list` is a list of symbols and intervals of\n    /// candlesticks in seconds.\n    async fn subscribe_candlestick(&self, symbol_interval_list: &[(String, usize)]);\n}\n\nmacro_rules! impl_trait {\n    ($trait_name:ident, $struct_name:ident, $method_name:ident, $channel:expr) => {\n        #[async_trait]\n        impl $trait_name for $struct_name {\n            async fn $method_name(&self, symbols: &[String]) {\n                let topics = symbols\n                    .iter()\n                    .map(|symbol| ($channel.to_string(), symbol.to_string()))\n                    .collect::<Vec<(String, String)>>();\n                self.subscribe(&topics).await;\n            }\n        }\n    };\n}\n\nmacro_rules! impl_candlestick {\n    ($struct_name:ident) => {\n        #[async_trait]\n        impl Candlestick for $struct_name {\n            async fn subscribe_candlestick(&self, symbol_interval_list: &[(String, usize)]) {\n                let commands =\n                    self.translator.translate_to_candlestick_commands(true, symbol_interval_list);\n                self.client.send(&commands).await;\n            }\n        }\n    };\n}\n\nmacro_rules! panic_ticker {\n    ($struct_name:ident) => {\n        #[async_trait]\n        impl Ticker for $struct_name {\n            async fn subscribe_ticker(&self, _symbols: &[String]) {\n                panic!(\"{} does NOT have the ticker websocket channel\", EXCHANGE_NAME);\n            }\n        }\n    };\n}\n\nmacro_rules! panic_bbo {\n    ($struct_name:ident) => {\n        #[async_trait]\n        impl BBO for $struct_name {\n            async fn subscribe_bbo(&self, _symbols: &[String]) {\n                panic!(\"{} does NOT have the BBO websocket channel\", EXCHANGE_NAME);\n            }\n        }\n    };\n}\n\nmacro_rules! panic_l2 {\n    ($struct_name:ident) => {\n        #[async_trait]\n        impl OrderBook for $struct_name {\n            async fn subscribe_orderbook(&self, _symbols: &[String]) {\n                panic!(\"{} does NOT have the incremental level2 websocket channel\", EXCHANGE_NAME);\n            }\n        }\n    };\n}\n\nmacro_rules! panic_l2_topk {\n    ($struct_name:ident) => {\n        #[async_trait]\n        impl OrderBookTopK for $struct_name {\n            async fn subscribe_orderbook_topk(&self, _symbols: &[String]) {\n                panic!(\n                    \"{} does NOT have the level2 top-k snapshot websocket channel\",\n                    EXCHANGE_NAME\n                );\n            }\n        }\n    };\n}\n\nmacro_rules! panic_l3_orderbook {\n    ($struct_name:ident) => {\n        #[async_trait]\n        impl Level3OrderBook for $struct_name {\n            async fn subscribe_l3_orderbook(&self, _symbols: &[String]) {\n                panic!(\"{} does NOT have the level3 websocket channel\", EXCHANGE_NAME);\n            }\n        }\n    };\n}\n\nmacro_rules! panic_candlestick {\n    ($struct_name:ident) => {\n        #[async_trait]\n        impl Candlestick for $struct_name {\n            async fn subscribe_candlestick(&self, _symbol_interval_list: &[(String, usize)]) {\n                panic!(\"{} does NOT have the candlestick websocket channel\", EXCHANGE_NAME);\n            }\n        }\n    };\n}\n\n/// Implement the new() constructor.\nmacro_rules! impl_new_constructor {\n    ($struct_name:ident, $exchange:ident, $default_url:expr, $handler:expr, $translator:expr) => {\n        impl $struct_name {\n            /// Creates a websocket client.\n            ///\n            /// # Arguments\n            ///\n            /// * `tx` - The sending part of a channel\n            /// * `url` - Optional server url, usually you don't need specify it\n            pub async fn new(tx: std::sync::mpsc::Sender<String>, url: Option<&str>) -> Self {\n                let real_url = match url {\n                    Some(endpoint) => endpoint,\n                    None => $default_url,\n                };\n                $struct_name {\n                    client: WSClientInternal::connect($exchange, real_url, $handler, None, tx)\n                        .await,\n                    translator: $translator,\n                }\n            }\n        }\n    };\n}\n\n/// Implement the WSClient trait.\nmacro_rules! impl_ws_client_trait {\n    ($struct_name:ident) => {\n        #[async_trait]\n        impl WSClient for $struct_name {\n            async fn subscribe_trade(&self, symbols: &[String]) {\n                <$struct_name as Trade>::subscribe_trade(self, symbols).await\n            }\n\n            async fn subscribe_orderbook(&self, symbols: &[String]) {\n                <$struct_name as OrderBook>::subscribe_orderbook(self, symbols).await\n            }\n\n            async fn subscribe_orderbook_topk(&self, symbols: &[String]) {\n                <$struct_name as OrderBookTopK>::subscribe_orderbook_topk(self, symbols).await\n            }\n\n            async fn subscribe_l3_orderbook(&self, symbols: &[String]) {\n                <$struct_name as Level3OrderBook>::subscribe_l3_orderbook(self, symbols).await\n            }\n\n            async fn subscribe_ticker(&self, symbols: &[String]) {\n                <$struct_name as Ticker>::subscribe_ticker(self, symbols).await\n            }\n\n            async fn subscribe_bbo(&self, symbols: &[String]) {\n                <$struct_name as BBO>::subscribe_bbo(self, symbols).await\n            }\n\n            async fn subscribe_candlestick(&self, symbol_interval_list: &[(String, usize)]) {\n                <$struct_name as Candlestick>::subscribe_candlestick(self, symbol_interval_list)\n                    .await\n            }\n\n            async fn subscribe(&self, topics: &[(String, String)]) {\n                let commands = self.translator.translate_to_commands(true, topics);\n                self.client.send(&commands).await;\n            }\n\n            async fn unsubscribe(&self, topics: &[(String, String)]) {\n                let commands = self.translator.translate_to_commands(false, topics);\n                self.client.send(&commands).await;\n            }\n\n            async fn send(&self, commands: &[String]) {\n                self.client.send(commands).await;\n            }\n\n            async fn run(&self) {\n                self.client.run().await;\n            }\n\n            async fn close(&self) {\n                self.client.close().await;\n            }\n        }\n    };\n}\n"
  },
  {
    "path": "crypto-ws-client/src/clients/deribit.rs",
    "content": "use async_trait::async_trait;\nuse std::collections::HashMap;\nuse tokio_tungstenite::tungstenite::Message;\n\nuse crate::{\n    clients::common_traits::{\n        Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO,\n    },\n    common::{\n        command_translator::CommandTranslator,\n        message_handler::{MessageHandler, MiscMessage},\n        utils::{ensure_frame_size, topic_to_raw_channel},\n        ws_client_internal::WSClientInternal,\n    },\n    WSClient,\n};\n\nuse log::*;\nuse serde_json::Value;\n\npub(super) const EXCHANGE_NAME: &str = \"deribit\";\n\nconst WEBSOCKET_URL: &str = \"wss://www.deribit.com/ws/api/v2/\";\n\n// -32600\t\"request entity too large\"\n/// single frame in websocket connection frame exceeds the limit (32 kB)\nconst WS_FRAME_SIZE: usize = 32 * 1024;\n\n/// The WebSocket client for Deribit.\n///\n/// Deribit has InverseFuture, InverseSwap and Option markets.\n///\n/// * WebSocket API doc: <https://docs.deribit.com/?shell#subscriptions>\n/// * Trading at:\n///     * Future <https://www.deribit.com/main#/futures>\n///     * Option <https://www.deribit.com/main#/options>\npub struct DeribitWSClient {\n    client: WSClientInternal<DeribitMessageHandler>,\n    translator: DeribitCommandTranslator,\n}\n\nimpl_new_constructor!(\n    DeribitWSClient,\n    EXCHANGE_NAME,\n    WEBSOCKET_URL,\n    DeribitMessageHandler {},\n    DeribitCommandTranslator {}\n);\n\n#[rustfmt::skip]\nimpl_trait!(Trade, DeribitWSClient, subscribe_trade, \"trades.SYMBOL.100ms\");\n#[rustfmt::skip]\nimpl_trait!(Ticker, DeribitWSClient, subscribe_ticker, \"ticker.SYMBOL.100ms\");\n#[rustfmt::skip]\nimpl_trait!(OrderBook, DeribitWSClient, subscribe_orderbook, \"book.SYMBOL.100ms\");\n#[rustfmt::skip]\nimpl_trait!(OrderBookTopK, DeribitWSClient, subscribe_orderbook_topk, \"book.SYMBOL.none.20.100ms\");\nimpl_trait!(BBO, DeribitWSClient, subscribe_bbo, \"quote.SYMBOL\");\n\nimpl_candlestick!(DeribitWSClient);\n\npanic_l3_orderbook!(DeribitWSClient);\n\nimpl_ws_client_trait!(DeribitWSClient);\n\nstruct DeribitMessageHandler {}\nstruct DeribitCommandTranslator {}\n\nimpl DeribitCommandTranslator {\n    fn topics_to_command(topics: &[(String, String)], subscribe: bool) -> String {\n        let raw_channels = topics.iter().map(topic_to_raw_channel).collect::<Vec<String>>();\n        format!(\n            r#\"{{\"method\": \"public/{}\", \"params\": {{\"channels\": {}}}}}\"#,\n            if subscribe { \"subscribe\" } else { \"unsubscribe\" },\n            serde_json::to_string(&raw_channels).unwrap()\n        )\n    }\n\n    fn to_candlestick_channel(interval: usize) -> String {\n        let interval_str = match interval {\n            60 => \"1\",\n            180 => \"3\",\n            300 => \"5\",\n            600 => \"10\",\n            900 => \"15\",\n            1800 => \"30\",\n            3600 => \"60\",\n            7200 => \"120\",\n            10800 => \"180\",\n            21600 => \"360\",\n            43200 => \"720\",\n            86400 => \"1D\",\n            _ => panic!(\"Unknown interval {interval}\"),\n        };\n        format!(\"chart.trades.SYMBOL.{interval_str}\")\n    }\n}\n\nimpl MessageHandler for DeribitMessageHandler {\n    fn handle_message(&mut self, msg: &str) -> MiscMessage {\n        let obj = serde_json::from_str::<HashMap<String, Value>>(msg).unwrap();\n\n        if obj.contains_key(\"error\") {\n            panic!(\"Received {msg} from {EXCHANGE_NAME}\");\n        } else if obj.contains_key(\"result\") {\n            info!(\"Received {} from {}\", msg, EXCHANGE_NAME);\n            MiscMessage::Other\n        } else if obj.contains_key(\"method\") && obj.contains_key(\"params\") {\n            match obj.get(\"method\").unwrap().as_str().unwrap() {\n                \"subscription\" => MiscMessage::Normal,\n                \"heartbeat\" => {\n                    let param_type = obj\n                        .get(\"params\")\n                        .unwrap()\n                        .as_object()\n                        .unwrap()\n                        .get(\"type\")\n                        .unwrap()\n                        .as_str()\n                        .unwrap();\n                    if param_type == \"test_request\" {\n                        let ws_msg = Message::Text(r#\"{\"method\": \"public/test\"}\"#.to_string());\n                        MiscMessage::WebSocket(ws_msg)\n                    } else {\n                        info!(\"Received {} from {}\", msg, EXCHANGE_NAME);\n                        MiscMessage::Other\n                    }\n                }\n                _ => {\n                    warn!(\"Received {} from {}\", msg, EXCHANGE_NAME);\n                    MiscMessage::Other\n                }\n            }\n        } else {\n            error!(\"Received {} from {}\", msg, EXCHANGE_NAME);\n            MiscMessage::Other\n        }\n    }\n\n    fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)> {\n        None\n    }\n}\n\nimpl CommandTranslator for DeribitCommandTranslator {\n    fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec<String> {\n        let mut all_commands: Vec<String> =\n            ensure_frame_size(topics, subscribe, Self::topics_to_command, WS_FRAME_SIZE, None);\n\n        all_commands\n            .push(r#\"{\"method\": \"public/set_heartbeat\", \"params\": {\"interval\": 10}}\"#.to_string());\n\n        all_commands\n    }\n\n    fn translate_to_candlestick_commands(\n        &self,\n        subscribe: bool,\n        symbol_interval_list: &[(String, usize)],\n    ) -> Vec<String> {\n        let topics = symbol_interval_list\n            .iter()\n            .map(|(symbol, interval)| (Self::to_candlestick_channel(*interval), symbol.clone()))\n            .collect::<Vec<(String, String)>>();\n        self.translate_to_commands(subscribe, &topics)\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use crate::common::command_translator::CommandTranslator;\n\n    #[test]\n    fn test_one_channel() {\n        let translator = super::DeribitCommandTranslator {};\n        let commands = translator.translate_to_commands(\n            true,\n            &[(\"trades.SYMBOL.100ms\".to_string(), \"BTC-26MAR21\".to_string())],\n        );\n\n        assert_eq!(2, commands.len());\n        assert_eq!(\n            r#\"{\"method\": \"public/subscribe\", \"params\": {\"channels\": [\"trades.BTC-26MAR21.100ms\"]}}\"#,\n            commands[0]\n        );\n        assert_eq!(\n            r#\"{\"method\": \"public/set_heartbeat\", \"params\": {\"interval\": 10}}\"#,\n            commands[1]\n        );\n    }\n\n    #[test]\n    fn test_two_channel() {\n        let translator = super::DeribitCommandTranslator {};\n        let commands = translator.translate_to_commands(\n            true,\n            &[\n                (\"trades.SYMBOL.100ms\".to_string(), \"BTC-26MAR21\".to_string()),\n                (\"ticker.SYMBOL.100ms\".to_string(), \"BTC-26MAR21\".to_string()),\n            ],\n        );\n\n        assert_eq!(2, commands.len());\n        assert_eq!(\n            r#\"{\"method\": \"public/subscribe\", \"params\": {\"channels\": [\"trades.BTC-26MAR21.100ms\",\"ticker.BTC-26MAR21.100ms\"]}}\"#,\n            commands[0]\n        );\n        assert_eq!(\n            r#\"{\"method\": \"public/set_heartbeat\", \"params\": {\"interval\": 10}}\"#,\n            commands[1]\n        );\n    }\n}\n"
  },
  {
    "path": "crypto-ws-client/src/clients/dydx/dydx_swap.rs",
    "content": "use async_trait::async_trait;\nuse std::collections::HashMap;\nuse tokio_tungstenite::tungstenite::Message;\n\nuse crate::{\n    clients::common_traits::{\n        Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO,\n    },\n    common::{\n        command_translator::CommandTranslator,\n        message_handler::{MessageHandler, MiscMessage},\n        ws_client_internal::WSClientInternal,\n    },\n    WSClient,\n};\n\nuse super::EXCHANGE_NAME;\nuse log::*;\nuse serde_json::Value;\n\nconst WEBSOCKET_URL: &str = \"wss://api.dydx.exchange/v3/ws\";\n\n/// The WebSocket client for dYdX perpetual markets.\n///\n/// * WebSocket API doc: <https://docs.dydx.exchange/#v3-websocket-api>\n/// * Trading at: <https://trade.dydx.exchange/trade>\npub struct DydxSwapWSClient {\n    client: WSClientInternal<DydxMessageHandler>,\n    translator: DydxCommandTranslator,\n}\n\nimpl_new_constructor!(\n    DydxSwapWSClient,\n    EXCHANGE_NAME,\n    WEBSOCKET_URL,\n    DydxMessageHandler {},\n    DydxCommandTranslator {}\n);\n\nimpl_trait!(Trade, DydxSwapWSClient, subscribe_trade, \"v3_trades\");\n#[rustfmt::skip]\nimpl_trait!(OrderBook, DydxSwapWSClient, subscribe_orderbook, \"v3_orderbook\");\n\npanic_ticker!(DydxSwapWSClient);\npanic_bbo!(DydxSwapWSClient);\npanic_l2_topk!(DydxSwapWSClient);\npanic_l3_orderbook!(DydxSwapWSClient);\npanic_candlestick!(DydxSwapWSClient);\n\nimpl_ws_client_trait!(DydxSwapWSClient);\n\nstruct DydxMessageHandler {}\nstruct DydxCommandTranslator {}\n\nimpl MessageHandler for DydxMessageHandler {\n    fn handle_message(&mut self, msg: &str) -> MiscMessage {\n        let obj = serde_json::from_str::<HashMap<String, Value>>(msg).unwrap();\n\n        match obj.get(\"type\").unwrap().as_str().unwrap() {\n            \"error\" => {\n                error!(\"Received {} from {}\", msg, EXCHANGE_NAME);\n                if obj.contains_key(\"message\")\n                    && obj\n                        .get(\"message\")\n                        .unwrap()\n                        .as_str()\n                        .unwrap()\n                        .starts_with(\"Invalid subscription id for channel\")\n                {\n                    panic!(\"Received {msg} from {EXCHANGE_NAME}\");\n                } else {\n                    MiscMessage::Other\n                }\n            }\n            \"connected\" | \"pong\" => {\n                debug!(\"Received {} from {}\", msg, EXCHANGE_NAME);\n                MiscMessage::Other\n            }\n            \"channel_data\" | \"subscribed\" => MiscMessage::Normal,\n            _ => {\n                warn!(\"Received {} from {}\", msg, EXCHANGE_NAME);\n                MiscMessage::Other\n            }\n        }\n    }\n\n    fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)> {\n        // https://docs.dydx.exchange/#v3-websocket-api\n        // The server will send pings every 30s and expects a pong within 10s.\n        // The server does not expect pings, but will respond with a pong if sent one.\n        Some((Message::Text(r#\"{\"type\":\"ping\"}\"#.to_string()), 30))\n    }\n}\n\nimpl DydxCommandTranslator {\n    fn topic_to_command(topic: &(String, String), subscribe: bool) -> String {\n        format!(\n            r#\"{{\"type\": \"{}\", \"channel\": \"{}\", \"id\": \"{}\"}}\"#,\n            if subscribe { \"subscribe\" } else { \"unsubscribe\" },\n            topic.0,\n            topic.1,\n        )\n    }\n}\n\nimpl CommandTranslator for DydxCommandTranslator {\n    fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec<String> {\n        topics.iter().map(|t| Self::topic_to_command(t, subscribe)).collect()\n    }\n\n    fn translate_to_candlestick_commands(\n        &self,\n        _subscribe: bool,\n        _symbol_interval_list: &[(String, usize)],\n    ) -> Vec<String> {\n        panic!(\"dYdX does NOT have candlestick channel\");\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use crate::common::command_translator::CommandTranslator;\n\n    #[test]\n    fn test_one_topic() {\n        let translator = super::DydxCommandTranslator {};\n        let commands = translator\n            .translate_to_commands(true, &[(\"v3_trades\".to_string(), \"BTC-USD\".to_string())]);\n\n        assert_eq!(1, commands.len());\n        assert_eq!(\n            r#\"{\"type\": \"subscribe\", \"channel\": \"v3_trades\", \"id\": \"BTC-USD\"}\"#,\n            commands[0]\n        );\n    }\n\n    #[test]\n    fn test_two_topic() {\n        let translator = super::DydxCommandTranslator {};\n        let commands = translator.translate_to_commands(\n            true,\n            &[\n                (\"v3_trades\".to_string(), \"BTC-USD\".to_string()),\n                (\"v3_orderbook\".to_string(), \"BTC-USD\".to_string()),\n            ],\n        );\n\n        assert_eq!(2, commands.len());\n        assert_eq!(\n            r#\"{\"type\": \"subscribe\", \"channel\": \"v3_trades\", \"id\": \"BTC-USD\"}\"#,\n            commands[0]\n        );\n        assert_eq!(\n            r#\"{\"type\": \"subscribe\", \"channel\": \"v3_orderbook\", \"id\": \"BTC-USD\"}\"#,\n            commands[1]\n        );\n    }\n}\n"
  },
  {
    "path": "crypto-ws-client/src/clients/dydx/mod.rs",
    "content": "mod dydx_swap;\n\npub use dydx_swap::DydxSwapWSClient;\n\nconst EXCHANGE_NAME: &str = \"dydx\";\n"
  },
  {
    "path": "crypto-ws-client/src/clients/ftx.rs",
    "content": "use async_trait::async_trait;\nuse std::collections::HashMap;\nuse tokio_tungstenite::tungstenite::Message;\n\nuse crate::{\n    clients::common_traits::{\n        Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO,\n    },\n    common::{\n        command_translator::CommandTranslator,\n        message_handler::{MessageHandler, MiscMessage},\n        ws_client_internal::WSClientInternal,\n    },\n    WSClient,\n};\n\nuse log::*;\nuse serde_json::Value;\n\npub(super) const EXCHANGE_NAME: &str = \"ftx\";\n\nconst WEBSOCKET_URL: &str = \"wss://ftx.com/ws/\";\n\n/// The WebSocket client for FTX.\n///\n/// FTX has Spot, LinearFuture, LinearSwap, Option, Move and BVOL markets.\n///\n/// * WebSocket API doc: <https://docs.ftx.com/#websocket-api>\n/// * Trading at <https://ftx.com/markets>\npub struct FtxWSClient {\n    client: WSClientInternal<FtxMessageHandler>,\n    translator: FtxCommandTranslator,\n}\n\nimpl_new_constructor!(\n    FtxWSClient,\n    EXCHANGE_NAME,\n    WEBSOCKET_URL,\n    FtxMessageHandler {},\n    FtxCommandTranslator {}\n);\n\nimpl_trait!(Trade, FtxWSClient, subscribe_trade, \"trades\");\nimpl_trait!(BBO, FtxWSClient, subscribe_bbo, \"ticker\");\n#[rustfmt::skip]\nimpl_trait!(OrderBook, FtxWSClient, subscribe_orderbook, \"orderbook\");\npanic_candlestick!(FtxWSClient);\npanic_l2_topk!(FtxWSClient);\npanic_l3_orderbook!(FtxWSClient);\npanic_ticker!(FtxWSClient);\n\nimpl_ws_client_trait!(FtxWSClient);\n\nstruct FtxMessageHandler {}\nstruct FtxCommandTranslator {}\n\nimpl MessageHandler for FtxMessageHandler {\n    fn handle_message(&mut self, msg: &str) -> MiscMessage {\n        let obj = serde_json::from_str::<HashMap<String, Value>>(msg).unwrap();\n        let msg_type = obj.get(\"type\").unwrap().as_str().unwrap();\n\n        match msg_type {\n            // see https://docs.ftx.com/#response-format\n            \"pong\" => MiscMessage::Pong,\n            \"subscribed\" | \"unsubscribed\" | \"info\" => {\n                info!(\"Received {} from {}\", msg, EXCHANGE_NAME);\n                MiscMessage::Other\n            }\n            \"partial\" | \"update\" => MiscMessage::Normal,\n            \"error\" => {\n                let code = obj.get(\"code\").unwrap().as_i64().unwrap();\n                match code {\n                    400 => {\n                        // Already subscribed\n                        warn!(\"Received {} from {}\", msg, EXCHANGE_NAME);\n                    }\n                    _ => panic!(\"Received {msg} from {EXCHANGE_NAME}\"),\n                }\n                MiscMessage::Other\n            }\n            _ => {\n                warn!(\"Received {} from {}\", msg, EXCHANGE_NAME);\n                MiscMessage::Other\n            }\n        }\n    }\n\n    fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)> {\n        // Send pings at regular intervals (every 15 seconds): {'op': 'ping'}.\n        // You will see an {'type': 'pong'} response.\n        Some((Message::Text(r#\"{\"op\":\"ping\"}\"#.to_string()), 15))\n    }\n}\n\nimpl CommandTranslator for FtxCommandTranslator {\n    fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec<String> {\n        topics\n            .iter()\n            .map(|(channel, symbol)| {\n                format!(\n                    r#\"{{\"op\":\"{}\",\"channel\":\"{}\",\"market\":\"{}\"}}\"#,\n                    if subscribe { \"subscribe\" } else { \"unsubscribe\" },\n                    channel,\n                    symbol\n                )\n            })\n            .collect()\n    }\n\n    fn translate_to_candlestick_commands(\n        &self,\n        _subscribe: bool,\n        _symbol_interval_list: &[(String, usize)],\n    ) -> Vec<String> {\n        panic!(\"FTX does NOT have candlestick channel\");\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use crate::common::command_translator::CommandTranslator;\n\n    #[test]\n    fn test_one_topic() {\n        let translator = super::FtxCommandTranslator {};\n        let commands = translator\n            .translate_to_commands(true, &[(\"trades\".to_string(), \"BTC/USD\".to_string())]);\n\n        assert_eq!(1, commands.len());\n        assert_eq!(r#\"{\"op\":\"subscribe\",\"channel\":\"trades\",\"market\":\"BTC/USD\"}\"#, commands[0]);\n    }\n\n    #[test]\n    fn test_two_topic() {\n        let translator = super::FtxCommandTranslator {};\n        let commands = translator.translate_to_commands(\n            true,\n            &[\n                (\"trades\".to_string(), \"BTC/USD\".to_string()),\n                (\"orderbook\".to_string(), \"BTC/USD\".to_string()),\n            ],\n        );\n\n        assert_eq!(2, commands.len());\n        assert_eq!(r#\"{\"op\":\"subscribe\",\"channel\":\"trades\",\"market\":\"BTC/USD\"}\"#, commands[0]);\n        assert_eq!(r#\"{\"op\":\"subscribe\",\"channel\":\"orderbook\",\"market\":\"BTC/USD\"}\"#, commands[1]);\n    }\n}\n"
  },
  {
    "path": "crypto-ws-client/src/clients/gate/gate_future.rs",
    "content": "use async_trait::async_trait;\n\nuse super::utils::{GateCommandTranslator, GateMessageHandler, EXCHANGE_NAME};\nuse crate::{\n    clients::common_traits::{\n        Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO,\n    },\n    common::{command_translator::CommandTranslator, ws_client_internal::WSClientInternal},\n    WSClient,\n};\n\nconst INVERSE_FUTURE_WEBSOCKET_URL: &str = \"wss://fx-ws.gateio.ws/v4/ws/delivery/btc\";\nconst LINEAR_FUTURE_WEBSOCKET_URL: &str = \"wss://fx-ws.gateio.ws/v4/ws/delivery/usdt\";\n\n/// The WebSocket client for Gate InverseFuture market.\n///\n/// * WebSocket API doc: <https://www.gate.io/docs/developers/delivery/ws/en/>\n/// * Trading at <https://www.gate.io/futures-delivery/btc>\npub struct GateInverseFutureWSClient {\n    client: WSClientInternal<GateMessageHandler<'F'>>,\n    translator: GateCommandTranslator<'F'>,\n}\n\n/// The WebSocket client for Gate LinearFuture market.\n///\n/// * WebSocket API doc: <https://www.gate.io/docs/developers/delivery/ws/en/>\n/// * Trading at <https://www.gate.io/futures-delivery/usdt>\npub struct GateLinearFutureWSClient {\n    client: WSClientInternal<GateMessageHandler<'F'>>,\n    translator: GateCommandTranslator<'F'>,\n}\n\nimpl_new_constructor!(\n    GateInverseFutureWSClient,\n    EXCHANGE_NAME,\n    INVERSE_FUTURE_WEBSOCKET_URL,\n    GateMessageHandler::<'F'> {},\n    GateCommandTranslator::<'F'> {}\n);\n\nimpl_new_constructor!(\n    GateLinearFutureWSClient,\n    EXCHANGE_NAME,\n    LINEAR_FUTURE_WEBSOCKET_URL,\n    GateMessageHandler::<'F'> {},\n    GateCommandTranslator::<'F'> {}\n);\n\nimpl_trait!(Trade, GateInverseFutureWSClient, subscribe_trade, \"trades\");\n#[rustfmt::skip]\nimpl_trait!(OrderBook, GateInverseFutureWSClient, subscribe_orderbook, \"order_book\");\n#[rustfmt::skip]\nimpl_trait!(Ticker, GateInverseFutureWSClient, subscribe_ticker, \"tickers\");\n\n#[rustfmt::skip]\nimpl_trait!(Trade, GateLinearFutureWSClient, subscribe_trade, \"trades\");\n#[rustfmt::skip]\nimpl_trait!(OrderBook, GateLinearFutureWSClient, subscribe_orderbook, \"order_book\");\n#[rustfmt::skip]\nimpl_trait!(Ticker, GateLinearFutureWSClient, subscribe_ticker, \"tickers\");\n\nimpl_candlestick!(GateInverseFutureWSClient);\nimpl_candlestick!(GateLinearFutureWSClient);\n\npanic_bbo!(GateInverseFutureWSClient);\npanic_bbo!(GateLinearFutureWSClient);\npanic_l2_topk!(GateInverseFutureWSClient);\npanic_l2_topk!(GateLinearFutureWSClient);\npanic_l3_orderbook!(GateInverseFutureWSClient);\npanic_l3_orderbook!(GateLinearFutureWSClient);\n\nimpl_ws_client_trait!(GateInverseFutureWSClient);\nimpl_ws_client_trait!(GateLinearFutureWSClient);\n"
  },
  {
    "path": "crypto-ws-client/src/clients/gate/gate_spot.rs",
    "content": "use async_trait::async_trait;\n\nuse super::utils::{GateCommandTranslator, GateMessageHandler, EXCHANGE_NAME};\nuse crate::{\n    clients::common_traits::{\n        Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO,\n    },\n    common::{command_translator::CommandTranslator, ws_client_internal::WSClientInternal},\n    WSClient,\n};\n\nconst WEBSOCKET_URL: &str = \"wss://api.gateio.ws/ws/v4/\";\n\n/// The WebSocket client for Gate spot market.\n///\n/// * WebSocket API doc: <https://www.gate.io/docs/developers/apiv4/ws/en/>\n/// * Trading at <https://www.gate.io/trade/BTC_USDT>\npub struct GateSpotWSClient {\n    client: WSClientInternal<GateMessageHandler<'S'>>,\n    translator: GateCommandTranslator<'S'>,\n}\n\nimpl_new_constructor!(\n    GateSpotWSClient,\n    EXCHANGE_NAME,\n    WEBSOCKET_URL,\n    GateMessageHandler::<'S'> {},\n    GateCommandTranslator::<'S'> {}\n);\n\nimpl_trait!(Trade, GateSpotWSClient, subscribe_trade, \"trades\");\n#[rustfmt::skip]\nimpl_trait!(OrderBook, GateSpotWSClient, subscribe_orderbook, \"order_book_update\");\n#[rustfmt::skip]\nimpl_trait!(OrderBookTopK, GateSpotWSClient, subscribe_orderbook_topk, \"order_book\");\nimpl_trait!(BBO, GateSpotWSClient, subscribe_bbo, \"book_ticker\");\nimpl_trait!(Ticker, GateSpotWSClient, subscribe_ticker, \"tickers\");\n\nimpl_candlestick!(GateSpotWSClient);\n\npanic_l3_orderbook!(GateSpotWSClient);\n\nimpl_ws_client_trait!(GateSpotWSClient);\n"
  },
  {
    "path": "crypto-ws-client/src/clients/gate/gate_swap.rs",
    "content": "use async_trait::async_trait;\n\nuse super::utils::{GateCommandTranslator, GateMessageHandler, EXCHANGE_NAME};\nuse crate::{\n    clients::common_traits::{\n        Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO,\n    },\n    common::{command_translator::CommandTranslator, ws_client_internal::WSClientInternal},\n    WSClient,\n};\n\nconst INVERSE_SWAP_WEBSOCKET_URL: &str = \"wss://fx-ws.gateio.ws/v4/ws/btc\";\nconst LINEAR_SWAP_WEBSOCKET_URL: &str = \"wss://fx-ws.gateio.ws/v4/ws/usdt\";\n\n/// The WebSocket client for Gate InverseSwap market.\n///\n/// * WebSocket API doc: <https://www.gate.io/docs/developers/futures/ws/en/>\n/// * Trading at <https://www.gate.io/futures_trade/BTC/BTC_USD>\npub struct GateInverseSwapWSClient {\n    client: WSClientInternal<GateMessageHandler<'F'>>,\n    translator: GateCommandTranslator<'F'>,\n}\n\n/// The WebSocket client for Gate LinearSwap market.\n///\n/// * WebSocket API doc: <https://www.gate.io/docs/developers/futures/ws/en/>\n/// * Trading at <https://www.gate.io/futures_trade/BTC/BTC_USDT>\npub struct GateLinearSwapWSClient {\n    client: WSClientInternal<GateMessageHandler<'F'>>,\n    translator: GateCommandTranslator<'F'>,\n}\n\nimpl_new_constructor!(\n    GateInverseSwapWSClient,\n    EXCHANGE_NAME,\n    INVERSE_SWAP_WEBSOCKET_URL,\n    GateMessageHandler::<'F'> {},\n    GateCommandTranslator::<'F'> {}\n);\n\nimpl_new_constructor!(\n    GateLinearSwapWSClient,\n    EXCHANGE_NAME,\n    LINEAR_SWAP_WEBSOCKET_URL,\n    GateMessageHandler::<'F'> {},\n    GateCommandTranslator::<'F'> {}\n);\n\nimpl_trait!(Trade, GateInverseSwapWSClient, subscribe_trade, \"trades\");\n#[rustfmt::skip]\nimpl_trait!(OrderBook, GateInverseSwapWSClient, subscribe_orderbook, \"order_book_update\");\n#[rustfmt::skip]\nimpl_trait!(OrderBookTopK, GateInverseSwapWSClient, subscribe_orderbook_topk, \"order_book\");\nimpl_trait!(BBO, GateInverseSwapWSClient, subscribe_bbo, \"book_ticker\");\nimpl_trait!(Ticker, GateInverseSwapWSClient, subscribe_ticker, \"tickers\");\n\nimpl_trait!(Trade, GateLinearSwapWSClient, subscribe_trade, \"trades\");\n#[rustfmt::skip]\nimpl_trait!(OrderBook, GateLinearSwapWSClient, subscribe_orderbook, \"order_book_update\");\n#[rustfmt::skip]\nimpl_trait!(OrderBookTopK, GateLinearSwapWSClient, subscribe_orderbook_topk, \"order_book\");\nimpl_trait!(BBO, GateLinearSwapWSClient, subscribe_bbo, \"book_ticker\");\nimpl_trait!(Ticker, GateLinearSwapWSClient, subscribe_ticker, \"tickers\");\n\nimpl_candlestick!(GateInverseSwapWSClient);\nimpl_candlestick!(GateLinearSwapWSClient);\n\npanic_l3_orderbook!(GateInverseSwapWSClient);\npanic_l3_orderbook!(GateLinearSwapWSClient);\n\nimpl_ws_client_trait!(GateInverseSwapWSClient);\nimpl_ws_client_trait!(GateLinearSwapWSClient);\n"
  },
  {
    "path": "crypto-ws-client/src/clients/gate/mod.rs",
    "content": "mod gate_future;\nmod gate_spot;\nmod gate_swap;\nmod utils;\n\npub use gate_future::{GateInverseFutureWSClient, GateLinearFutureWSClient};\npub use gate_spot::GateSpotWSClient;\npub use gate_swap::{GateInverseSwapWSClient, GateLinearSwapWSClient};\n"
  },
  {
    "path": "crypto-ws-client/src/clients/gate/utils.rs",
    "content": "use std::collections::HashMap;\n\nuse log::*;\nuse serde_json::Value;\nuse tokio_tungstenite::tungstenite::Message;\n\nuse crate::common::{\n    command_translator::CommandTranslator,\n    message_handler::{MessageHandler, MiscMessage},\n};\n\npub(super) const EXCHANGE_NAME: &str = \"gate\";\n\n// MARKET_TYPE: 'S' for spot, 'F' for futures\npub(super) struct GateMessageHandler<const MARKET_TYPE: char> {}\npub(super) struct GateCommandTranslator<const MARKET_TYPE: char> {}\n\nimpl<const MARKET_TYPE: char> MessageHandler for GateMessageHandler<MARKET_TYPE> {\n    fn handle_message(&mut self, msg: &str) -> MiscMessage {\n        let obj = serde_json::from_str::<HashMap<String, Value>>(msg).unwrap();\n\n        // https://www.gate.io/docs/apiv4/ws/en/#server-response\n        // Null if the server accepts the client request; otherwise, the detailed reason\n        // why request is rejected.\n        let error = match obj.get(\"error\") {\n            None => serde_json::Value::Null,\n            Some(err) => {\n                if err.is_null() {\n                    serde_json::Value::Null\n                } else {\n                    err.clone()\n                }\n            }\n        };\n        if !error.is_null() {\n            let err = error.as_object().unwrap();\n            // https://www.gate.io/docs/apiv4/ws/en/#schema_error\n            // https://www.gate.io/docs/futures/ws/en/#error\n            let code = err.get(\"code\").unwrap().as_i64().unwrap();\n            match code {\n                1 | 2 => panic!(\"Received {msg} from {EXCHANGE_NAME}\"), // client side errors\n                _ => error!(\"Received {} from {}\", msg, EXCHANGE_NAME), // server side errors\n            }\n            return MiscMessage::Other;\n        }\n\n        let channel = obj.get(\"channel\").unwrap().as_str().unwrap();\n        let event = obj.get(\"event\").unwrap().as_str().unwrap();\n\n        if channel == \"spot.pong\" || channel == \"futures.pong\" {\n            MiscMessage::Pong\n        } else if event == \"update\" || event == \"all\" {\n            MiscMessage::Normal\n        } else if event == \"subscribe\" || event == \"unsubscribe\" {\n            debug!(\"Received {} from {}\", msg, EXCHANGE_NAME);\n            MiscMessage::Other\n        } else {\n            warn!(\"Received {} from {}\", msg, EXCHANGE_NAME);\n            MiscMessage::Other\n        }\n    }\n\n    fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)> {\n        if MARKET_TYPE == 'S' {\n            // https://www.gate.io/docs/apiv4/ws/en/#application-ping-pong\n            Some((Message::Text(r#\"{\"channel\":\"spot.ping\"}\"#.to_string()), 60))\n        } else {\n            // https://www.gate.io/docs/futures/ws/en/#ping-and-pong\n            // https://www.gate.io/docs/delivery/ws/en/#ping-and-pong\n            Some((Message::Text(r#\"{\"channel\":\"futures.ping\"}\"#.to_string()), 60))\n        }\n    }\n}\n\nimpl<const MARKET_TYPE: char> GateCommandTranslator<MARKET_TYPE> {\n    fn channel_symbols_to_command(\n        channel: &str,\n        symbols: &[String],\n        subscribe: bool,\n    ) -> Vec<String> {\n        let channel = if MARKET_TYPE == 'S' {\n            format!(\"spot.{channel}\")\n        } else if MARKET_TYPE == 'F' {\n            format!(\"futures.{channel}\")\n        } else {\n            panic!(\"unexpected market type: {MARKET_TYPE}\")\n        };\n        if channel.contains(\".order_book\") {\n            symbols\n                .iter()\n                .map(|symbol| {\n                    format!(\n                        r#\"{{\"channel\":\"{}\", \"event\":\"{}\", \"payload\":{}}}\"#,\n                        channel,\n                        if subscribe { \"subscribe\" } else { \"unsubscribe\" },\n                        if channel.ends_with(\".order_book\") {\n                            if MARKET_TYPE == 'S' {\n                                serde_json::to_string(&[symbol, \"20\", \"1000ms\"]).unwrap()\n                            } else if MARKET_TYPE == 'F' {\n                                serde_json::to_string(&[symbol, \"20\", \"0\"]).unwrap()\n                            } else {\n                                panic!(\"unexpected market type: {MARKET_TYPE}\")\n                            }\n                        } else if channel.ends_with(\".order_book_update\") {\n                            if MARKET_TYPE == 'S' {\n                                serde_json::to_string(&[symbol, \"100ms\"]).unwrap()\n                            } else if MARKET_TYPE == 'F' {\n                                serde_json::to_string(&[symbol, \"100ms\", \"20\"]).unwrap()\n                            } else {\n                                panic!(\"unexpected market type: {MARKET_TYPE}\")\n                            }\n                        } else {\n                            panic!(\"unexpected channel: {channel}\")\n                        },\n                    )\n                })\n                .collect()\n        } else {\n            vec![format!(\n                r#\"{{\"channel\":\"{}\", \"event\":\"{}\", \"payload\":{}}}\"#,\n                channel,\n                if subscribe { \"subscribe\" } else { \"unsubscribe\" },\n                serde_json::to_string(&symbols).unwrap(),\n            )]\n        }\n    }\n\n    fn to_candlestick_command(symbol: &str, interval: usize, subscribe: bool) -> String {\n        let interval_str = match interval {\n            10 => \"10s\",\n            60 => \"1m\",\n            300 => \"5m\",\n            900 => \"15m\",\n            1800 => \"30m\",\n            3600 => \"1h\",\n            14400 => \"4h\",\n            28800 => \"8h\",\n            86400 => \"1d\",\n            604800 => \"7d\",\n            _ => panic!(\"Gate available intervals 10s,1m,5m,15m,30m,1h,4h,8h,1d,7d\"),\n        };\n        format!(\n            r#\"{{\"channel\": \"{}.candlesticks\", \"event\": \"{}\", \"payload\" : [\"{}\", \"{}\"]}}\"#,\n            if MARKET_TYPE == 'S' { \"spot\" } else { \"futures\" },\n            if subscribe { \"subscribe\" } else { \"unsubscribe\" },\n            interval_str,\n            symbol\n        )\n    }\n}\n\nimpl<const MARKET_TYPE: char> CommandTranslator for GateCommandTranslator<MARKET_TYPE> {\n    fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec<String> {\n        let mut commands: Vec<String> = Vec::new();\n\n        let mut channel_symbols = HashMap::<String, Vec<String>>::new();\n        for (channel, symbol) in topics {\n            match channel_symbols.get_mut(channel) {\n                Some(symbols) => symbols.push(symbol.to_string()),\n                None => {\n                    channel_symbols.insert(channel.to_string(), vec![symbol.to_string()]);\n                }\n            }\n        }\n\n        for (channel, symbols) in channel_symbols.iter() {\n            commands.extend(Self::channel_symbols_to_command(channel, symbols, subscribe));\n        }\n\n        commands\n    }\n\n    fn translate_to_candlestick_commands(\n        &self,\n        subscribe: bool,\n        symbol_interval_list: &[(String, usize)],\n    ) -> Vec<String> {\n        symbol_interval_list\n            .iter()\n            .map(|(symbol, interval)| Self::to_candlestick_command(symbol, *interval, subscribe))\n            .collect::<Vec<String>>()\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use crate::common::command_translator::CommandTranslator;\n\n    #[test]\n    fn test_spot() {\n        let translator = super::GateCommandTranslator::<'S'> {};\n\n        assert_eq!(\n            r#\"{\"channel\":\"spot.trades\", \"event\":\"subscribe\", \"payload\":[\"BTC_USDT\",\"ETH_USDT\"]}\"#,\n            translator.translate_to_commands(\n                true,\n                &[\n                    (\"trades\".to_string(), \"BTC_USDT\".to_string()),\n                    (\"trades\".to_string(), \"ETH_USDT\".to_string())\n                ]\n            )[0]\n        );\n\n        let commands = translator.translate_to_commands(\n            true,\n            &[\n                (\"order_book\".to_string(), \"BTC_USDT\".to_string()),\n                (\"order_book\".to_string(), \"ETH_USDT\".to_string()),\n            ],\n        );\n        assert_eq!(2, commands.len());\n        assert_eq!(\n            r#\"{\"channel\":\"spot.order_book\", \"event\":\"subscribe\", \"payload\":[\"BTC_USDT\",\"20\",\"1000ms\"]}\"#,\n            commands[0]\n        );\n        assert_eq!(\n            r#\"{\"channel\":\"spot.order_book\", \"event\":\"subscribe\", \"payload\":[\"ETH_USDT\",\"20\",\"1000ms\"]}\"#,\n            commands[1]\n        );\n\n        let commands = translator.translate_to_commands(\n            true,\n            &[\n                (\"order_book_update\".to_string(), \"BTC_USDT\".to_string()),\n                (\"order_book_update\".to_string(), \"ETH_USDT\".to_string()),\n            ],\n        );\n        assert_eq!(2, commands.len());\n        assert_eq!(\n            r#\"{\"channel\":\"spot.order_book_update\", \"event\":\"subscribe\", \"payload\":[\"BTC_USDT\",\"100ms\"]}\"#,\n            commands[0]\n        );\n        assert_eq!(\n            r#\"{\"channel\":\"spot.order_book_update\", \"event\":\"subscribe\", \"payload\":[\"ETH_USDT\",\"100ms\"]}\"#,\n            commands[1]\n        );\n    }\n\n    #[test]\n    fn test_futures() {\n        let translator = super::GateCommandTranslator::<'F'> {};\n\n        assert_eq!(\n            r#\"{\"channel\":\"futures.trades\", \"event\":\"subscribe\", \"payload\":[\"BTC_USD\",\"ETH_USD\"]}\"#,\n            translator.translate_to_commands(\n                true,\n                &[\n                    (\"trades\".to_string(), \"BTC_USD\".to_string()),\n                    (\"trades\".to_string(), \"ETH_USD\".to_string())\n                ]\n            )[0]\n        );\n\n        let commands = translator.translate_to_commands(\n            true,\n            &[\n                (\"order_book\".to_string(), \"BTC_USD\".to_string()),\n                (\"order_book\".to_string(), \"ETH_USD\".to_string()),\n            ],\n        );\n        assert_eq!(2, commands.len());\n        assert_eq!(\n            r#\"{\"channel\":\"futures.order_book\", \"event\":\"subscribe\", \"payload\":[\"BTC_USD\",\"20\",\"0\"]}\"#,\n            commands[0]\n        );\n        assert_eq!(\n            r#\"{\"channel\":\"futures.order_book\", \"event\":\"subscribe\", \"payload\":[\"ETH_USD\",\"20\",\"0\"]}\"#,\n            commands[1]\n        );\n\n        let commands = translator.translate_to_commands(\n            true,\n            &[\n                (\"order_book_update\".to_string(), \"BTC_USD\".to_string()),\n                (\"order_book_update\".to_string(), \"ETH_USD\".to_string()),\n            ],\n        );\n        assert_eq!(2, commands.len());\n        assert_eq!(\n            r#\"{\"channel\":\"futures.order_book_update\", \"event\":\"subscribe\", \"payload\":[\"BTC_USD\",\"100ms\",\"20\"]}\"#,\n            commands[0]\n        );\n        assert_eq!(\n            r#\"{\"channel\":\"futures.order_book_update\", \"event\":\"subscribe\", \"payload\":[\"ETH_USD\",\"100ms\",\"20\"]}\"#,\n            commands[1]\n        );\n    }\n}\n"
  },
  {
    "path": "crypto-ws-client/src/clients/huobi.rs",
    "content": "use async_trait::async_trait;\nuse std::collections::HashMap;\n\nuse log::*;\nuse serde_json::Value;\nuse tokio_tungstenite::tungstenite::Message;\n\nuse crate::{\n    common::{\n        command_translator::CommandTranslator,\n        message_handler::{MessageHandler, MiscMessage},\n        ws_client_internal::WSClientInternal,\n    },\n    WSClient,\n};\n\npub(crate) const EXCHANGE_NAME: &str = \"huobi\";\n\n// or wss://api-aws.huobi.pro/feed\nconst SPOT_WEBSOCKET_URL: &str = \"wss://api.huobi.pro/ws\";\n// const FUTURES_WEBSOCKET_URL: &str = \"wss://www.hbdm.com/ws\";\n// const COIN_SWAP_WEBSOCKET_URL: &str = \"wss://api.hbdm.com/swap-ws\";\n// const USDT_SWAP_WEBSOCKET_URL: &str = \"wss://api.hbdm.com/linear-swap-ws\";\n// const OPTION_WEBSOCKET_URL: &str = \"wss://api.hbdm.com/option-ws\";\nconst FUTURES_WEBSOCKET_URL: &str = \"wss://futures.huobi.com/ws\";\nconst COIN_SWAP_WEBSOCKET_URL: &str = \"wss://futures.huobi.com/swap-ws\";\nconst USDT_SWAP_WEBSOCKET_URL: &str = \"wss://futures.huobi.com/linear-swap-ws\";\nconst OPTION_WEBSOCKET_URL: &str = \"wss://futures.huobi.com/option-ws\";\n\n// Internal unified client\npub struct HuobiWSClient<const URL: char> {\n    client: WSClientInternal<HuobiMessageHandler>,\n    translator: HuobiCommandTranslator,\n}\n\n/// Huobi Spot market.\n///\n/// * WebSocket API doc: <https://huobiapi.github.io/docs/spot/v1/en/>\n/// * Trading at: <https://www.huobi.com/en-us/exchange/>\npub type HuobiSpotWSClient = HuobiWSClient<'S'>;\n\n/// Huobi Future market.\n///\n/// * WebSocket API doc: <https://huobiapi.github.io/docs/dm/v1/en/>\n/// * Trading at: <https://futures.huobi.com/en-us/contract/exchange/>\npub type HuobiFutureWSClient = HuobiWSClient<'F'>;\n\n/// Huobi Inverse Swap market.\n///\n/// Inverse Swap market uses coins like BTC as collateral.\n///\n/// * WebSocket API doc: <https://huobiapi.github.io/docs/coin_margined_swap/v1/en/>\n/// * Trading at: <https://futures.huobi.com/en-us/swap/exchange/>\npub type HuobiInverseSwapWSClient = HuobiWSClient<'I'>;\n\n/// Huobi Linear Swap market.\n///\n/// Linear Swap market uses USDT as collateral.\n///\n/// * WebSocket API doc: <https://huobiapi.github.io/docs/usdt_swap/v1/en/>\n/// * Trading at: <https://futures.huobi.com/en-us/linear_swap/exchange/>\npub type HuobiLinearSwapWSClient = HuobiWSClient<'L'>;\n\n/// Huobi Option market.\n///\n///\n/// * WebSocket API doc: <https://huobiapi.github.io/docs/option/v1/en/>\n/// * Trading at: <https://futures.huobi.com/en-us/option/exchange/>\npub type HuobiOptionWSClient = HuobiWSClient<'O'>;\n\nimpl<const URL: char> HuobiWSClient<URL> {\n    pub async fn new(tx: std::sync::mpsc::Sender<String>, url: Option<&str>) -> Self {\n        let real_url = match url {\n            Some(endpoint) => endpoint,\n            None => {\n                if URL == 'S' {\n                    SPOT_WEBSOCKET_URL\n                } else if URL == 'F' {\n                    FUTURES_WEBSOCKET_URL\n                } else if URL == 'I' {\n                    COIN_SWAP_WEBSOCKET_URL\n                } else if URL == 'L' {\n                    USDT_SWAP_WEBSOCKET_URL\n                } else if URL == 'O' {\n                    OPTION_WEBSOCKET_URL\n                } else {\n                    panic!(\"Unknown URL {URL}\");\n                }\n            }\n        };\n        HuobiWSClient {\n            client: WSClientInternal::connect(\n                EXCHANGE_NAME,\n                real_url,\n                HuobiMessageHandler {},\n                None,\n                tx,\n            )\n            .await,\n            translator: HuobiCommandTranslator {},\n        }\n    }\n}\n\n#[async_trait]\nimpl<const URL: char> WSClient for HuobiWSClient<URL> {\n    async fn subscribe_trade(&self, symbols: &[String]) {\n        let topics = symbols\n            .iter()\n            .map(|symbol| (\"trade.detail\".to_string(), symbol.to_string()))\n            .collect::<Vec<(String, String)>>();\n        self.subscribe(&topics).await;\n    }\n\n    async fn subscribe_orderbook(&self, symbols: &[String]) {\n        if URL == 'S' {\n            let topics = symbols\n                .iter()\n                .map(|symbol| (\"mbp.20\".to_string(), symbol.to_string()))\n                .collect::<Vec<(String, String)>>();\n            self.subscribe(&topics).await;\n        } else {\n            let commands = symbols\n                .iter()\n                .map(|symbol| format!(r#\"{{\"sub\":\"market.{symbol}.depth.size_20.high_freq\",\"data_type\":\"incremental\",\"id\": \"crypto-ws-client\"}}\"#))\n                .collect::<Vec<String>>();\n            self.client.send(&commands).await;\n        }\n    }\n\n    async fn subscribe_orderbook_topk(&self, symbols: &[String]) {\n        let channel = if URL == 'S' { \"depth.step1\" } else { \"depth.step7\" };\n        let topics = symbols\n            .iter()\n            .map(|symbol| (channel.to_string(), symbol.to_string()))\n            .collect::<Vec<(String, String)>>();\n        self.subscribe(&topics).await;\n    }\n\n    async fn subscribe_l3_orderbook(&self, _symbols: &[String]) {\n        panic!(\"{EXCHANGE_NAME} does NOT have the level3 websocket channel\");\n    }\n\n    async fn subscribe_ticker(&self, symbols: &[String]) {\n        let topics = symbols\n            .iter()\n            .map(|symbol| (\"detail\".to_string(), symbol.to_string()))\n            .collect::<Vec<(String, String)>>();\n        self.subscribe(&topics).await;\n    }\n\n    async fn subscribe_bbo(&self, symbols: &[String]) {\n        let topics = symbols\n            .iter()\n            .map(|symbol| (\"bbo\".to_string(), symbol.to_string()))\n            .collect::<Vec<(String, String)>>();\n        self.subscribe(&topics).await;\n    }\n\n    async fn subscribe_candlestick(&self, symbol_interval_list: &[(String, usize)]) {\n        let commands =\n            self.translator.translate_to_candlestick_commands(true, symbol_interval_list);\n        self.client.send(&commands).await;\n    }\n\n    async fn subscribe(&self, topics: &[(String, String)]) {\n        let commands = self.translator.translate_to_commands(true, topics);\n        self.client.send(&commands).await;\n    }\n\n    async fn unsubscribe(&self, topics: &[(String, String)]) {\n        let commands = self.translator.translate_to_commands(false, topics);\n        self.client.send(&commands).await;\n    }\n\n    async fn send(&self, commands: &[String]) {\n        self.client.send(commands).await;\n    }\n\n    async fn run(&self) {\n        self.client.run().await;\n    }\n\n    async fn close(&self) {\n        self.client.close().await;\n    }\n}\n\nstruct HuobiMessageHandler {}\nstruct HuobiCommandTranslator {}\n\nimpl HuobiCommandTranslator {\n    fn topic_to_command(channel: &str, symbol: &str, subscribe: bool) -> String {\n        let raw_channel = format!(\"market.{symbol}.{channel}\");\n        format!(\n            r#\"{{\"{}\":\"{}\",\"id\":\"crypto-ws-client\"}}\"#,\n            if subscribe { \"sub\" } else { \"unsub\" },\n            raw_channel\n        )\n    }\n\n    // see https://huobiapi.github.io/docs/dm/v1/en/#subscribe-kline-data\n    fn to_candlestick_raw_channel(interval: usize) -> String {\n        let interval_str = match interval {\n            60 => \"1min\",\n            300 => \"5min\",\n            900 => \"15min\",\n            1800 => \"30min\",\n            3600 => \"60min\",\n            14400 => \"4hour\",\n            86400 => \"1day\",\n            604800 => \"1week\",\n            2592000 => \"1mon\",\n            _ => panic!(\"Huobi has intervals 1min,5min,15min,30min,60min,4hour,1day,1week,1mon\"),\n        };\n        format!(\"kline.{interval_str}\")\n    }\n}\n\nimpl MessageHandler for HuobiMessageHandler {\n    fn handle_message(&mut self, msg: &str) -> MiscMessage {\n        let resp = serde_json::from_str::<HashMap<String, Value>>(msg);\n        if resp.is_err() {\n            error!(\"{} is not a JSON string, {}\", msg, EXCHANGE_NAME);\n            return MiscMessage::Other;\n        }\n        let obj = resp.unwrap();\n\n        // Market Heartbeat\n        if obj.contains_key(\"ping\") {\n            // The server will send a heartbeat every 5 seconds\n            // see links in get_ping_msg_and_interval()S\n            debug!(\"Received {} from {}\", msg, EXCHANGE_NAME);\n            let timestamp = obj.get(\"ping\").unwrap();\n            let mut pong_msg = HashMap::<String, &Value>::new();\n            pong_msg.insert(\"pong\".to_string(), timestamp);\n            let ws_msg = Message::Text(serde_json::to_string(&pong_msg).unwrap());\n            return MiscMessage::WebSocket(ws_msg);\n        }\n        // Order Push Heartbeat\n        // https://huobiapi.github.io/docs/usdt_swap/v1/en/#market-heartbeat\n        if obj.contains_key(\"op\") && obj.get(\"op\").unwrap().as_str().unwrap() == \"ping\" {\n            debug!(\"Received {} from {}\", msg, EXCHANGE_NAME);\n            let mut pong_msg = obj;\n            pong_msg.insert(\"op\".to_string(), serde_json::from_str(\"\\\"pong\\\"\").unwrap()); // change ping to pong\n            let ws_msg = Message::Text(serde_json::to_string(&pong_msg).unwrap());\n            return MiscMessage::WebSocket(ws_msg);\n        }\n\n        if (obj.contains_key(\"ch\") || obj.contains_key(\"topic\"))\n            && obj.contains_key(\"ts\")\n            && (obj.contains_key(\"tick\") || obj.contains_key(\"data\"))\n        {\n            MiscMessage::Normal\n        } else {\n            if let Some(status) = obj.get(\"status\") {\n                match status.as_str().unwrap() {\n                    \"ok\" => info!(\"Received {} from {}\", msg, EXCHANGE_NAME),\n                    \"error\" => {\n                        error!(\"Received {} from {}\", msg, EXCHANGE_NAME);\n                        let err_msg = obj.get(\"err-msg\").unwrap().as_str().unwrap();\n                        if err_msg.starts_with(\"invalid\") {\n                            panic!(\"Received {msg} from {EXCHANGE_NAME}\");\n                        }\n                    }\n                    _ => warn!(\"Received {} from {}\", msg, EXCHANGE_NAME),\n                }\n            } else if let Some(op) = obj.get(\"op\") {\n                match op.as_str().unwrap() {\n                    \"sub\" | \"unsub\" => MiscMessage::Other,\n                    \"notify\" => MiscMessage::Normal,\n                    _ => {\n                        warn!(\"Received {} from {}\", msg, EXCHANGE_NAME);\n                        MiscMessage::Other\n                    }\n                };\n            } else {\n                warn!(\"Received {} from {}\", msg, EXCHANGE_NAME);\n            }\n            MiscMessage::Other\n        }\n    }\n\n    fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)> {\n        // The server will send a heartbeat every 5 seconds,\n        // - Spot <https://huobiapi.github.io/docs/spot/v1/en/#heartbeat-and-connection>\n        // - Future <https://huobiapi.github.io/docs/dm/v1/en/#market-heartbeat>\n        // - InverseSwap <https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#market-heartbeat>\n        // - LinearSwap <https://huobiapi.github.io/docs/usdt_swap/v1/en/#market-heartbeat>\n        // - Option <https://huobiapi.github.io/docs/option/v1/en/#market-heartbeat>\n        None\n    }\n}\n\nimpl CommandTranslator for HuobiCommandTranslator {\n    fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec<String> {\n        topics\n            .iter()\n            .map(|(channel, symbol)| {\n                HuobiCommandTranslator::topic_to_command(channel, symbol, subscribe)\n            })\n            .collect()\n    }\n\n    fn translate_to_candlestick_commands(\n        &self,\n        subscribe: bool,\n        symbol_interval_list: &[(String, usize)],\n    ) -> Vec<String> {\n        let topics = symbol_interval_list\n            .iter()\n            .map(|(symbol, interval)| {\n                let channel = Self::to_candlestick_raw_channel(*interval);\n                (channel, symbol.to_string())\n            })\n            .collect::<Vec<(String, String)>>();\n        self.translate_to_commands(subscribe, &topics)\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use crate::common::command_translator::CommandTranslator;\n\n    #[test]\n    fn test_one_topic() {\n        let translator = super::HuobiCommandTranslator {};\n        let commands = translator\n            .translate_to_commands(true, &[(\"trade.detail\".to_string(), \"btcusdt\".to_string())]);\n\n        assert_eq!(1, commands.len());\n        assert_eq!(r#\"{\"sub\":\"market.btcusdt.trade.detail\",\"id\":\"crypto-ws-client\"}\"#, commands[0]);\n    }\n\n    #[test]\n    fn test_two_topics() {\n        let translator = super::HuobiCommandTranslator {};\n        let commands = translator.translate_to_commands(\n            true,\n            &[\n                (\"trade.detail\".to_string(), \"btcusdt\".to_string()),\n                (\"bbo\".to_string(), \"btcusdt\".to_string()),\n            ],\n        );\n\n        assert_eq!(2, commands.len());\n        assert_eq!(r#\"{\"sub\":\"market.btcusdt.trade.detail\",\"id\":\"crypto-ws-client\"}\"#, commands[0]);\n        assert_eq!(r#\"{\"sub\":\"market.btcusdt.bbo\",\"id\":\"crypto-ws-client\"}\"#, commands[1]);\n    }\n}\n"
  },
  {
    "path": "crypto-ws-client/src/clients/kraken/kraken_futures.rs",
    "content": "use async_trait::async_trait;\nuse std::collections::HashMap;\nuse tokio_tungstenite::tungstenite::Message;\n\nuse super::EXCHANGE_NAME;\nuse crate::{\n    clients::common_traits::{\n        Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO,\n    },\n    common::{\n        command_translator::CommandTranslator,\n        message_handler::{MessageHandler, MiscMessage},\n        ws_client_internal::WSClientInternal,\n    },\n    WSClient,\n};\nuse log::*;\nuse serde_json::Value;\n\n// https://support.kraken.com/hc/en-us/articles/360022839491-API-URLs\nconst WEBSOCKET_URL: &str = \"wss://futures.kraken.com/ws/v1\";\n\n/// The WebSocket client for Kraken Futures market.\n///\n///\n///   * WebSocket API doc: <https://support.kraken.com/hc/en-us/sections/360003562371-Websocket-API-Public>\n///   * Trading at: <https://futures.kraken.com/>\npub struct KrakenFuturesWSClient {\n    client: WSClientInternal<KrakenMessageHandler>,\n    translator: KrakenCommandTranslator,\n}\n\nimpl_new_constructor!(\n    KrakenFuturesWSClient,\n    EXCHANGE_NAME,\n    WEBSOCKET_URL,\n    KrakenMessageHandler {},\n    KrakenCommandTranslator {}\n);\n\n#[rustfmt::skip]\nimpl_trait!(Trade, KrakenFuturesWSClient, subscribe_trade, \"trade\");\n#[rustfmt::skip]\nimpl_trait!(OrderBook, KrakenFuturesWSClient, subscribe_orderbook, \"book\");\n#[rustfmt::skip]\nimpl_trait!(Ticker, KrakenFuturesWSClient, subscribe_ticker, \"ticker\");\n\npanic_bbo!(KrakenFuturesWSClient);\npanic_l2_topk!(KrakenFuturesWSClient);\npanic_l3_orderbook!(KrakenFuturesWSClient);\npanic_candlestick!(KrakenFuturesWSClient);\n\nimpl_ws_client_trait!(KrakenFuturesWSClient);\n\nstruct KrakenMessageHandler {}\nstruct KrakenCommandTranslator {}\n\nimpl KrakenCommandTranslator {\n    fn channel_symbols_to_command(channel: &str, symbols: &[String], subscribe: bool) -> String {\n        format!(\n            r#\"{{\"event\":\"{}\",\"feed\":\"{}\",\"product_ids\":{}}}\"#,\n            if subscribe { \"subscribe\" } else { \"unsubscribe\" },\n            channel,\n            serde_json::to_string(symbols).unwrap(),\n        )\n    }\n}\n\nimpl MessageHandler for KrakenMessageHandler {\n    fn handle_message(&mut self, msg: &str) -> MiscMessage {\n        let obj = serde_json::from_str::<HashMap<String, Value>>(msg).unwrap();\n\n        if obj.contains_key(\"event\") {\n            let event = obj.get(\"event\").unwrap().as_str().unwrap();\n            match event {\n                \"error\" => panic!(\"Received {msg} from {EXCHANGE_NAME}\"),\n                \"info\" | \"subscribed\" | \"unsubscribed\" => {\n                    info!(\"Received {} from {}\", msg, EXCHANGE_NAME);\n                    MiscMessage::Other\n                }\n                _ => {\n                    warn!(\"Received {} from {}\", msg, EXCHANGE_NAME);\n                    MiscMessage::Other\n                }\n            }\n        } else if obj.contains_key(\"feed\") {\n            let feed = obj.get(\"feed\").unwrap().as_str().unwrap();\n            if feed == \"heartbeat\" {\n                debug!(\"Received {} from {}\", msg, EXCHANGE_NAME);\n                MiscMessage::WebSocket(Message::Ping(Vec::new()))\n            } else if obj.contains_key(\"product_id\") {\n                MiscMessage::Normal\n            } else {\n                MiscMessage::Other\n            }\n        } else {\n            MiscMessage::Other\n        }\n    }\n\n    fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)> {\n        // In order to keep the websocket connection alive, you will need to\n        // make a ping request at least every 60 seconds.\n        // https://support.kraken.com/hc/en-us/articles/360022635632-Subscriptions-WebSockets-API-\n        None // TODO: lack of doc\n    }\n}\n\nimpl CommandTranslator for KrakenCommandTranslator {\n    fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec<String> {\n        let mut commands: Vec<String> = Vec::new();\n\n        let mut channel_symbols = HashMap::<String, Vec<String>>::new();\n        for (channel, symbol) in topics {\n            match channel_symbols.get_mut(channel) {\n                Some(symbols) => symbols.push(symbol.to_string()),\n                None => {\n                    channel_symbols.insert(channel.to_string(), vec![symbol.to_string()]);\n                }\n            }\n        }\n\n        for (channel, symbols) in channel_symbols.iter() {\n            commands.push(Self::channel_symbols_to_command(channel, symbols, subscribe));\n        }\n        commands.push(r#\"{\"event\":\"subscribe\",\"feed\":\"heartbeat\"}\"#.to_string());\n\n        commands\n    }\n\n    fn translate_to_candlestick_commands(\n        &self,\n        _subscribe: bool,\n        _symbol_interval_list: &[(String, usize)],\n    ) -> Vec<String> {\n        panic!(\"Kraken Futures does NOT have candlestick channel\");\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use crate::common::command_translator::CommandTranslator;\n\n    #[test]\n    fn test_one_symbol() {\n        let translator = super::KrakenCommandTranslator {};\n        let commands = translator\n            .translate_to_commands(true, &[(\"trade\".to_string(), \"PI_XBTUSD\".to_string())]);\n\n        assert_eq!(2, commands.len());\n        assert_eq!(\n            r#\"{\"event\":\"subscribe\",\"feed\":\"trade\",\"product_ids\":[\"PI_XBTUSD\"]}\"#,\n            commands[0]\n        );\n        assert_eq!(r#\"{\"event\":\"subscribe\",\"feed\":\"heartbeat\"}\"#, commands[1]);\n    }\n\n    #[test]\n    fn test_two_symbols() {\n        let translator = super::KrakenCommandTranslator {};\n        let commands = translator.translate_to_commands(\n            true,\n            &[\n                (\"trade\".to_string(), \"PI_XBTUSD\".to_string()),\n                (\"trade\".to_string(), \"PI_ETHUSD\".to_string()),\n            ],\n        );\n\n        assert_eq!(2, commands.len());\n\n        assert_eq!(\n            r#\"{\"event\":\"subscribe\",\"feed\":\"trade\",\"product_ids\":[\"PI_XBTUSD\",\"PI_ETHUSD\"]}\"#,\n            commands[0]\n        );\n        assert_eq!(r#\"{\"event\":\"subscribe\",\"feed\":\"heartbeat\"}\"#, commands[1]);\n    }\n}\n"
  },
  {
    "path": "crypto-ws-client/src/clients/kraken/kraken_spot.rs",
    "content": "use async_trait::async_trait;\nuse std::collections::HashMap;\nuse tokio_tungstenite::tungstenite::Message;\n\nuse super::EXCHANGE_NAME;\nuse crate::{\n    clients::common_traits::{\n        Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO,\n    },\n    common::{\n        command_translator::CommandTranslator,\n        message_handler::{MessageHandler, MiscMessage},\n        ws_client_internal::WSClientInternal,\n    },\n    WSClient,\n};\n\nuse log::*;\nuse serde_json::Value;\n\nconst WEBSOCKET_URL: &str = \"wss://ws.kraken.com\";\n\n/// The WebSocket client for Kraken Spot market.\n///\n///\n///   * WebSocket API doc: <https://docs.kraken.com/websockets/>\n///   * Trading at: <https://trade.kraken.com/>\npub struct KrakenSpotWSClient {\n    client: WSClientInternal<KrakenMessageHandler>,\n    translator: KrakenCommandTranslator,\n}\n\nimpl_new_constructor!(\n    KrakenSpotWSClient,\n    EXCHANGE_NAME,\n    WEBSOCKET_URL,\n    KrakenMessageHandler {},\n    KrakenCommandTranslator {}\n);\n\n#[rustfmt::skip]\nimpl_trait!(Trade, KrakenSpotWSClient, subscribe_trade, \"trade\");\nimpl_trait!(OrderBook, KrakenSpotWSClient, subscribe_orderbook, \"book\");\n#[rustfmt::skip]\nimpl_trait!(Ticker, KrakenSpotWSClient, subscribe_ticker, \"ticker\");\n#[rustfmt::skip]\nimpl_trait!(BBO, KrakenSpotWSClient, subscribe_bbo, \"spread\");\nimpl_candlestick!(KrakenSpotWSClient);\n\npanic_l2_topk!(KrakenSpotWSClient);\npanic_l3_orderbook!(KrakenSpotWSClient);\n\nimpl_ws_client_trait!(KrakenSpotWSClient);\n\nstruct KrakenMessageHandler {}\nstruct KrakenCommandTranslator {}\n\nimpl MessageHandler for KrakenMessageHandler {\n    fn handle_message(&mut self, msg: &str) -> MiscMessage {\n        let resp = serde_json::from_str::<Value>(msg);\n        if resp.is_err() {\n            error!(\"{} is not a JSON string, {}\", msg, EXCHANGE_NAME);\n            return MiscMessage::Other;\n        }\n        let value = resp.unwrap();\n\n        if value.is_object() {\n            let obj = value.as_object().unwrap();\n            let event = obj.get(\"event\").unwrap().as_str().unwrap();\n            match event {\n                \"heartbeat\" => {\n                    debug!(\"Received {} from {}\", msg, EXCHANGE_NAME);\n                    let ping = r#\"{\n                      \"event\": \"ping\",\n                      \"reqid\": 9527\n                  }\"#;\n                    MiscMessage::WebSocket(Message::Text(ping.to_string()))\n                }\n                \"pong\" => MiscMessage::Pong,\n                \"subscriptionStatus\" => {\n                    let status = obj.get(\"status\").unwrap().as_str().unwrap();\n                    match status {\n                        \"subscribed\" | \"unsubscribed\" => {\n                            info!(\"Received {} from {}\", msg, EXCHANGE_NAME)\n                        }\n                        \"error\" => {\n                            let error_msg = obj.get(\"errorMessage\").unwrap().as_str().unwrap();\n                            if error_msg.starts_with(\"Currency pair not supported\") {\n                                // Sometimes currency pairs returned from RESTful API don't exist in\n                                // WebSocket yet\n                                error!(\"Received {} from {}\", msg, EXCHANGE_NAME)\n                            } else {\n                                panic!(\"Received {msg} from {EXCHANGE_NAME}\");\n                            }\n                        }\n                        _ => warn!(\"Received {} from {}\", msg, EXCHANGE_NAME),\n                    }\n\n                    MiscMessage::Other\n                }\n                \"systemStatus\" => {\n                    let status = obj.get(\"status\").unwrap().as_str().unwrap();\n                    match status {\n                        \"maintenance\" | \"cancel_only\" => {\n                            warn!(\"Received {}, which means Kraken is in maintenance mode\", msg);\n                            std::thread::sleep(std::time::Duration::from_secs(20));\n                            MiscMessage::Reconnect\n                        }\n                        _ => {\n                            info!(\"Received {} from {}\", msg, EXCHANGE_NAME);\n                            MiscMessage::Other\n                        }\n                    }\n                }\n                _ => {\n                    warn!(\"Received {} from {}\", msg, EXCHANGE_NAME);\n                    MiscMessage::Other\n                }\n            }\n        } else {\n            MiscMessage::Normal\n        }\n    }\n\n    fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)> {\n        // Client can ping server to determine whether connection is alive\n        // https://docs.kraken.com/websockets/#message-ping\n        Some((Message::Text(r#\"{\"event\":\"ping\"}\"#.to_string()), 10))\n    }\n}\n\nimpl KrakenCommandTranslator {\n    fn name_symbols_to_command(name: &str, symbols: &[String], subscribe: bool) -> String {\n        if name == \"book\" {\n            format!(\n                r#\"{{\"event\":\"{}\",\"pair\":{},\"subscription\":{{\"name\":\"{}\",\"depth\":25}}}}\"#,\n                if subscribe { \"subscribe\" } else { \"unsubscribe\" },\n                serde_json::to_string(symbols).unwrap(),\n                name\n            )\n        } else {\n            format!(\n                r#\"{{\"event\":\"{}\",\"pair\":{},\"subscription\":{{\"name\":\"{}\"}}}}\"#,\n                if subscribe { \"subscribe\" } else { \"unsubscribe\" },\n                serde_json::to_string(symbols).unwrap(),\n                name\n            )\n        }\n    }\n\n    fn convert_symbol_interval_list(\n        symbol_interval_list: &[(String, usize)],\n    ) -> Vec<(Vec<String>, usize)> {\n        let mut map = HashMap::<usize, Vec<String>>::new();\n        for task in symbol_interval_list {\n            let v = map.entry(task.1).or_insert_with(Vec::new);\n            v.push(task.0.clone());\n        }\n        let mut result = Vec::new();\n        for (k, v) in map {\n            result.push((v, k));\n        }\n        result\n    }\n}\n\nimpl CommandTranslator for KrakenCommandTranslator {\n    fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec<String> {\n        let mut commands: Vec<String> = Vec::new();\n\n        let mut channel_symbols = HashMap::<String, Vec<String>>::new();\n        for (channel, symbol) in topics {\n            match channel_symbols.get_mut(channel) {\n                Some(symbols) => symbols.push(symbol.to_string()),\n                None => {\n                    channel_symbols.insert(channel.to_string(), vec![symbol.to_string()]);\n                }\n            }\n        }\n\n        for (channel, symbols) in channel_symbols.iter() {\n            commands.push(Self::name_symbols_to_command(channel, symbols, subscribe));\n        }\n\n        commands\n    }\n\n    fn translate_to_candlestick_commands(\n        &self,\n        subscribe: bool,\n        symbol_interval_list: &[(String, usize)],\n    ) -> Vec<String> {\n        let valid_set: Vec<usize> =\n            vec![1, 5, 15, 30, 60, 240, 1440, 10080, 21600].into_iter().map(|x| x * 60).collect();\n        let invalid_intervals = symbol_interval_list\n            .iter()\n            .map(|(_, interval)| *interval)\n            .filter(|x| !valid_set.contains(x))\n            .collect::<Vec<usize>>();\n        if !invalid_intervals.is_empty() {\n            panic!(\n                \"Invalid intervals: {}, available intervals: {}\",\n                invalid_intervals\n                    .into_iter()\n                    .map(|x| x.to_string())\n                    .collect::<Vec<String>>()\n                    .join(\",\"),\n                valid_set.into_iter().map(|x| x.to_string()).collect::<Vec<String>>().join(\",\")\n            );\n        }\n        let symbols_interval_list = Self::convert_symbol_interval_list(symbol_interval_list);\n        let commands: Vec<String> = symbols_interval_list\n            .into_iter()\n            .map(|(symbols, interval)| {\n                format!(\n                    r#\"{{\"event\":\"{}\",\"pair\":{},\"subscription\":{{\"name\":\"ohlc\", \"interval\":{}}}}}\"#,\n                    if subscribe { \"subscribe\" } else { \"unsubscribe\" },\n                    serde_json::to_string(&symbols).unwrap(),\n                    interval / 60\n                )\n            })\n            .collect();\n\n        commands\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use crate::common::command_translator::CommandTranslator;\n\n    #[test]\n    fn test_one_symbol() {\n        let translator = super::KrakenCommandTranslator {};\n        let commands =\n            translator.translate_to_commands(true, &[(\"trade\".to_string(), \"XBT/USD\".to_string())]);\n\n        assert_eq!(1, commands.len());\n        assert_eq!(\n            r#\"{\"event\":\"subscribe\",\"pair\":[\"XBT/USD\"],\"subscription\":{\"name\":\"trade\"}}\"#,\n            commands[0]\n        );\n    }\n\n    #[test]\n    fn test_two_symbols() {\n        let translator = super::KrakenCommandTranslator {};\n        let commands = translator.translate_to_commands(\n            true,\n            &[\n                (\"trade\".to_string(), \"XBT/USD\".to_string()),\n                (\"trade\".to_string(), \"ETH/USD\".to_string()),\n            ],\n        );\n\n        assert_eq!(1, commands.len());\n        assert_eq!(\n            r#\"{\"event\":\"subscribe\",\"pair\":[\"XBT/USD\",\"ETH/USD\"],\"subscription\":{\"name\":\"trade\"}}\"#,\n            commands[0]\n        );\n    }\n}\n"
  },
  {
    "path": "crypto-ws-client/src/clients/kraken/mod.rs",
    "content": "mod kraken_futures;\nmod kraken_spot;\n\nconst EXCHANGE_NAME: &str = \"kraken\";\n\npub use kraken_futures::KrakenFuturesWSClient;\npub use kraken_spot::KrakenSpotWSClient;\n"
  },
  {
    "path": "crypto-ws-client/src/clients/kucoin/kucoin_spot.rs",
    "content": "use super::utils::{fetch_ws_token, KucoinMessageHandler, EXCHANGE_NAME, UPLINK_LIMIT};\nuse crate::{\n    clients::common_traits::{\n        Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO,\n    },\n    common::{command_translator::CommandTranslator, ws_client_internal::WSClientInternal},\n    WSClient,\n};\nuse async_trait::async_trait;\nuse std::sync::mpsc::Sender;\n\n/// The WebSocket client for KuCoin Spot market.\n///\n/// * WebSocket API doc: <https://docs.kucoin.com/#websocket-feed>\n/// * Trading at: <https://trade.kucoin.com/>\npub struct KuCoinSpotWSClient {\n    client: WSClientInternal<KucoinMessageHandler>,\n    translator: KucoinCommandTranslator,\n}\n\nimpl KuCoinSpotWSClient {\n    /// Creates a KuCoinSpotWSClient websocket client.\n    ///\n    /// # Arguments\n    ///\n    /// * `tx` - The sending part of a channel\n    /// * `url` - Optional server url, usually you don't need specify it\n    pub async fn new(tx: Sender<String>, url: Option<&str>) -> Self {\n        let real_url = match url {\n            Some(endpoint) => endpoint.to_string(),\n            None => {\n                let ws_token = fetch_ws_token().await;\n                let ws_url = format!(\"{}?token={}\", ws_token.endpoint, ws_token.token);\n                ws_url\n            }\n        };\n        KuCoinSpotWSClient {\n            client: WSClientInternal::connect(\n                EXCHANGE_NAME,\n                &real_url,\n                KucoinMessageHandler {},\n                Some(UPLINK_LIMIT),\n                tx,\n            )\n            .await,\n            translator: KucoinCommandTranslator {},\n        }\n    }\n}\n\nimpl_trait!(Trade, KuCoinSpotWSClient, subscribe_trade, \"/market/match\");\nimpl_trait!(BBO, KuCoinSpotWSClient, subscribe_bbo, \"/market/ticker\");\n#[rustfmt::skip]\nimpl_trait!(OrderBook, KuCoinSpotWSClient, subscribe_orderbook, \"/market/level2\");\n#[rustfmt::skip]\nimpl_trait!(OrderBookTopK, KuCoinSpotWSClient, subscribe_orderbook_topk, \"/spotMarket/level2Depth5\");\n#[rustfmt::skip]\nimpl_trait!(Ticker, KuCoinSpotWSClient, subscribe_ticker, \"/market/snapshot\");\n\nimpl_candlestick!(KuCoinSpotWSClient);\n\npanic_l3_orderbook!(KuCoinSpotWSClient);\n\nimpl_ws_client_trait!(KuCoinSpotWSClient);\n\nstruct KucoinCommandTranslator {}\n\nimpl KucoinCommandTranslator {\n    fn to_candlestick_channel(symbol: &str, interval: usize) -> String {\n        let interval_str = match interval {\n            60 => \"1min\",\n            180 => \"3min\",\n            300 => \"5min\",\n            900 => \"15min\",\n            1800 => \"30min\",\n            3600 => \"1hour\",\n            7200 => \"2hour\",\n            14400 => \"4hour\",\n            21600 => \"6hour\",\n            28800 => \"8hour\",\n            43200 => \"12hour\",\n            86400 => \"1day\",\n            604800 => \"1week\",\n            _ => panic!(\n                \"KuCoin available intervals 1min,3min,5min,15min,30min,1hour,2hour,4hour,6hour,8hour,12hour,1day,1week\"\n            ),\n        };\n        format!(\"{symbol}_{interval_str}\")\n    }\n}\n\nimpl CommandTranslator for KucoinCommandTranslator {\n    fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec<String> {\n        super::utils::topics_to_commands(topics, subscribe)\n    }\n\n    fn translate_to_candlestick_commands(\n        &self,\n        subscribe: bool,\n        symbol_interval_list: &[(String, usize)],\n    ) -> Vec<String> {\n        let topics = symbol_interval_list\n            .iter()\n            .map(|(symbol, interval)| {\n                (\"/market/candles\".to_string(), Self::to_candlestick_channel(symbol, *interval))\n            })\n            .collect::<Vec<(String, String)>>();\n        self.translate_to_commands(subscribe, &topics)\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use crate::common::command_translator::CommandTranslator;\n\n    #[test]\n    fn test_one_channel() {\n        let translator = super::KucoinCommandTranslator {};\n        let commands = translator\n            .translate_to_commands(true, &[(\"/market/match\".to_string(), \"BTC-USDT\".to_string())]);\n\n        assert_eq!(1, commands.len());\n        assert_eq!(\n            r#\"{\"id\":\"crypto-ws-client\",\"type\":\"subscribe\",\"topic\":\"/market/match:BTC-USDT\",\"privateChannel\":false,\"response\":true}\"#,\n            commands[0]\n        );\n\n        let commands = translator.translate_to_commands(\n            true,\n            &[\n                (\"/market/match\".to_string(), \"BTC-USDT\".to_string()),\n                (\"/market/match\".to_string(), \"ETH-USDT\".to_string()),\n            ],\n        );\n\n        assert_eq!(1, commands.len());\n        assert_eq!(\n            r#\"{\"id\":\"crypto-ws-client\",\"type\":\"subscribe\",\"topic\":\"/market/match:BTC-USDT,ETH-USDT\",\"privateChannel\":false,\"response\":true}\"#,\n            commands[0]\n        );\n    }\n\n    #[test]\n    fn test_two_channels() {\n        let translator = super::KucoinCommandTranslator {};\n        let commands = translator.translate_to_commands(\n            true,\n            &[\n                (\"/market/match\".to_string(), \"BTC-USDT\".to_string()),\n                (\"/market/level2\".to_string(), \"ETH-USDT\".to_string()),\n            ],\n        );\n\n        assert_eq!(2, commands.len());\n        assert_eq!(\n            r#\"{\"id\":\"crypto-ws-client\",\"type\":\"subscribe\",\"topic\":\"/market/level2:ETH-USDT\",\"privateChannel\":false,\"response\":true}\"#,\n            commands[0]\n        );\n        assert_eq!(\n            r#\"{\"id\":\"crypto-ws-client\",\"type\":\"subscribe\",\"topic\":\"/market/match:BTC-USDT\",\"privateChannel\":false,\"response\":true}\"#,\n            commands[1]\n        );\n\n        let commands = translator.translate_to_commands(\n            true,\n            &[\n                (\"/market/match\".to_string(), \"BTC-USDT\".to_string()),\n                (\"/market/match\".to_string(), \"ETH-USDT\".to_string()),\n                (\"/market/level2\".to_string(), \"BTC-USDT\".to_string()),\n                (\"/market/level2\".to_string(), \"ETH-USDT\".to_string()),\n            ],\n        );\n\n        assert_eq!(2, commands.len());\n        assert_eq!(\n            r#\"{\"id\":\"crypto-ws-client\",\"type\":\"subscribe\",\"topic\":\"/market/level2:BTC-USDT,ETH-USDT\",\"privateChannel\":false,\"response\":true}\"#,\n            commands[0]\n        );\n        assert_eq!(\n            r#\"{\"id\":\"crypto-ws-client\",\"type\":\"subscribe\",\"topic\":\"/market/match:BTC-USDT,ETH-USDT\",\"privateChannel\":false,\"response\":true}\"#,\n            commands[1]\n        );\n    }\n\n    #[test]\n    fn test_candlestick() {\n        let translator = super::KucoinCommandTranslator {};\n        let commands = translator.translate_to_candlestick_commands(\n            true,\n            &[(\"BTC-USDT\".to_string(), 180), (\"ETH-USDT\".to_string(), 60)],\n        );\n\n        assert_eq!(1, commands.len());\n        assert_eq!(\n            r#\"{\"id\":\"crypto-ws-client\",\"type\":\"subscribe\",\"topic\":\"/market/candles:BTC-USDT_3min,ETH-USDT_1min\",\"privateChannel\":false,\"response\":true}\"#,\n            commands[0]\n        );\n    }\n}\n"
  },
  {
    "path": "crypto-ws-client/src/clients/kucoin/kucoin_swap.rs",
    "content": "use super::utils::{fetch_ws_token, KucoinMessageHandler, EXCHANGE_NAME, UPLINK_LIMIT};\nuse crate::{\n    clients::common_traits::{\n        Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO,\n    },\n    common::{command_translator::CommandTranslator, ws_client_internal::WSClientInternal},\n    WSClient,\n};\nuse async_trait::async_trait;\nuse std::sync::mpsc::Sender;\n\n/// The WebSocket client for KuCoin Swap markets.\n///\n/// * WebSocket API doc: <https://docs.kucoin.cc/futures/#websocket-2>\n/// * Trading at: <https://futures.kucoin.com/>\npub struct KuCoinSwapWSClient {\n    client: WSClientInternal<KucoinMessageHandler>,\n    translator: KucoinCommandTranslator,\n}\n\nimpl KuCoinSwapWSClient {\n    /// Creates a KuCoinSwapWSClient websocket client.\n    ///\n    /// # Arguments\n    ///\n    /// * `tx` - The sending part of a channel\n    /// * `url` - Optional server url, usually you don't need specify it\n    pub async fn new(tx: Sender<String>, url: Option<&str>) -> Self {\n        let real_url = match url {\n            Some(endpoint) => endpoint.to_string(),\n            None => {\n                let ws_token = fetch_ws_token().await;\n                let ws_url = format!(\"{}?token={}\", ws_token.endpoint, ws_token.token);\n                ws_url\n            }\n        };\n        KuCoinSwapWSClient {\n            client: WSClientInternal::connect(\n                EXCHANGE_NAME,\n                &real_url,\n                KucoinMessageHandler {},\n                Some(UPLINK_LIMIT),\n                tx,\n            )\n            .await,\n            translator: KucoinCommandTranslator {},\n        }\n    }\n}\n\n#[rustfmt::skip]\nimpl_trait!(Trade, KuCoinSwapWSClient, subscribe_trade, \"/contractMarket/execution\");\n#[rustfmt::skip]\nimpl_trait!(BBO, KuCoinSwapWSClient, subscribe_bbo, \"/contractMarket/tickerV2\");\n#[rustfmt::skip]\nimpl_trait!(OrderBook, KuCoinSwapWSClient, subscribe_orderbook, \"/contractMarket/level2\");\n#[rustfmt::skip]\nimpl_trait!(OrderBookTopK, KuCoinSwapWSClient, subscribe_orderbook_topk, \"/contractMarket/level2Depth5\");\n#[rustfmt::skip]\nimpl_trait!(Ticker, KuCoinSwapWSClient, subscribe_ticker, \"/contractMarket/snapshot\");\n\nimpl_candlestick!(KuCoinSwapWSClient);\n\npanic_l3_orderbook!(KuCoinSwapWSClient);\n\nimpl_ws_client_trait!(KuCoinSwapWSClient);\n\nstruct KucoinCommandTranslator {}\n\nimpl KucoinCommandTranslator {\n    fn to_candlestick_channel(symbol: &str, interval: usize) -> String {\n        let valid_set: Vec<usize> =\n            vec![60, 300, 900, 1800, 3600, 7200, 14400, 28800, 43200, 86400, 604800];\n        if !valid_set.contains(&interval) {\n            let joined =\n                valid_set.into_iter().map(|x| x.to_string()).collect::<Vec<String>>().join(\",\");\n            panic!(\"KuCoin Swap available intervals {joined}\");\n        }\n        format!(\"{}_{}\", symbol, interval / 60)\n    }\n}\n\nimpl CommandTranslator for KucoinCommandTranslator {\n    fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec<String> {\n        super::utils::topics_to_commands(topics, subscribe)\n    }\n\n    fn translate_to_candlestick_commands(\n        &self,\n        subscribe: bool,\n        symbol_interval_list: &[(String, usize)],\n    ) -> Vec<String> {\n        let topics = symbol_interval_list\n            .iter()\n            .map(|(symbol, interval)| {\n                (\n                    \"/contractMarket/candle\".to_string(),\n                    Self::to_candlestick_channel(symbol, *interval),\n                )\n            })\n            .collect::<Vec<(String, String)>>();\n\n        self.translate_to_commands(subscribe, &topics)\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use crate::common::command_translator::CommandTranslator;\n\n    #[test]\n    fn test_one_channel() {\n        let translator = super::KucoinCommandTranslator {};\n        let commands = translator.translate_to_commands(\n            true,\n            &[(\"/contractMarket/execution\".to_string(), \"BTC_USD\".to_string())],\n        );\n\n        assert_eq!(1, commands.len());\n        assert_eq!(\n            r#\"{\"id\":\"crypto-ws-client\",\"type\":\"subscribe\",\"topic\":\"/contractMarket/execution:BTC_USD\",\"privateChannel\":false,\"response\":true}\"#,\n            commands[0]\n        );\n\n        let commands = translator.translate_to_commands(\n            true,\n            &[\n                (\"/contractMarket/execution\".to_string(), \"BTC_USD\".to_string()),\n                (\"/contractMarket/execution\".to_string(), \"ETH_USD\".to_string()),\n            ],\n        );\n\n        assert_eq!(1, commands.len());\n        assert_eq!(\n            r#\"{\"id\":\"crypto-ws-client\",\"type\":\"subscribe\",\"topic\":\"/contractMarket/execution:BTC_USD,ETH_USD\",\"privateChannel\":false,\"response\":true}\"#,\n            commands[0]\n        );\n    }\n\n    #[test]\n    fn test_two_channels() {\n        let translator = super::KucoinCommandTranslator {};\n        let commands = translator.translate_to_commands(\n            true,\n            &[\n                (\"/contractMarket/execution\".to_string(), \"BTC_USD\".to_string()),\n                (\"/contractMarket/level2\".to_string(), \"ETH_USD\".to_string()),\n            ],\n        );\n\n        assert_eq!(2, commands.len());\n        assert_eq!(\n            r#\"{\"id\":\"crypto-ws-client\",\"type\":\"subscribe\",\"topic\":\"/contractMarket/execution:BTC_USD\",\"privateChannel\":false,\"response\":true}\"#,\n            commands[0]\n        );\n        assert_eq!(\n            r#\"{\"id\":\"crypto-ws-client\",\"type\":\"subscribe\",\"topic\":\"/contractMarket/level2:ETH_USD\",\"privateChannel\":false,\"response\":true}\"#,\n            commands[1]\n        );\n\n        let commands = translator.translate_to_commands(\n            true,\n            &[\n                (\"/contractMarket/execution\".to_string(), \"BTC_USD\".to_string()),\n                (\"/contractMarket/execution\".to_string(), \"ETH_USD\".to_string()),\n                (\"/contractMarket/level2\".to_string(), \"BTC_USD\".to_string()),\n                (\"/contractMarket/level2\".to_string(), \"ETH_USD\".to_string()),\n            ],\n        );\n\n        assert_eq!(2, commands.len());\n        assert_eq!(\n            r#\"{\"id\":\"crypto-ws-client\",\"type\":\"subscribe\",\"topic\":\"/contractMarket/execution:BTC_USD,ETH_USD\",\"privateChannel\":false,\"response\":true}\"#,\n            commands[0]\n        );\n        assert_eq!(\n            r#\"{\"id\":\"crypto-ws-client\",\"type\":\"subscribe\",\"topic\":\"/contractMarket/level2:BTC_USD,ETH_USD\",\"privateChannel\":false,\"response\":true}\"#,\n            commands[1]\n        );\n    }\n\n    #[test]\n    fn test_candlestick() {\n        let translator = super::KucoinCommandTranslator {};\n        let commands = translator.translate_to_candlestick_commands(\n            true,\n            &[(\"BTC_USD\".to_string(), 300), (\"ETH_USD\".to_string(), 60)],\n        );\n\n        assert_eq!(1, commands.len());\n        assert_eq!(\n            r#\"{\"id\":\"crypto-ws-client\",\"type\":\"subscribe\",\"topic\":\"/contractMarket/candle:BTC_USD_5,ETH_USD_1\",\"privateChannel\":false,\"response\":true}\"#,\n            commands[0]\n        );\n    }\n}\n"
  },
  {
    "path": "crypto-ws-client/src/clients/kucoin/mod.rs",
    "content": "mod kucoin_spot;\nmod kucoin_swap;\nmod utils;\n\npub use kucoin_spot::KuCoinSpotWSClient;\npub use kucoin_swap::KuCoinSwapWSClient;\n"
  },
  {
    "path": "crypto-ws-client/src/clients/kucoin/utils.rs",
    "content": "use std::{\n    collections::{BTreeMap, HashMap},\n    num::NonZeroU32,\n};\n\nuse log::*;\nuse nonzero_ext::nonzero;\nuse reqwest::{header, Result};\nuse serde_json::Value;\nuse tokio_tungstenite::tungstenite::Message;\n\nuse crate::common::message_handler::{MessageHandler, MiscMessage};\n\npub(super) const EXCHANGE_NAME: &str = \"kucoin\";\n\n// Maximum number of batch subscriptions at a time: 100 topics\n// See https://docs.kucoin.com/#topic-subscription-limit\nconst MAX_TOPICS_PER_COMMAND: usize = 100;\n\n// Message limit sent to the server: 100 per 10 seconds, see https://docs.kucoin.cc/#request-rate-limit\npub(super) const UPLINK_LIMIT: (NonZeroU32, std::time::Duration) =\n    (nonzero!(100u32), std::time::Duration::from_secs(10));\n\npub(super) struct WebsocketToken {\n    pub token: String,\n    pub endpoint: String,\n}\n\nasync fn http_post(url: &str) -> Result<String> {\n    let mut headers = header::HeaderMap::new();\n    headers.insert(header::CONTENT_TYPE, header::HeaderValue::from_static(\"application/json\"));\n\n    let client = reqwest::Client::builder()\n         .default_headers(headers)\n         .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\")\n         .gzip(true)\n         .build()?;\n    let response = client.post(url).send().await?;\n\n    match response.error_for_status() {\n        Ok(resp) => Ok(resp.text().await?),\n        Err(error) => Err(error),\n    }\n}\n\n// See <https://docs.kucoin.com/#apply-connect-token>\npub(super) async fn fetch_ws_token() -> WebsocketToken {\n    let txt = http_post(\"https://openapi-v2.kucoin.com/api/v1/bullet-public\").await.unwrap();\n    let obj = serde_json::from_str::<HashMap<String, Value>>(&txt).unwrap();\n    let code = obj.get(\"code\").unwrap().as_str().unwrap();\n    if code != \"200000\" {\n        panic!(\"Failed to get token, code is {code}\");\n    }\n    let data = obj.get(\"data\").unwrap().as_object().unwrap();\n    let token = data.get(\"token\").unwrap().as_str().unwrap();\n    let servers = data.get(\"instanceServers\").unwrap().as_array().unwrap();\n    let server = servers[0].as_object().unwrap();\n\n    WebsocketToken {\n        token: token.to_string(),\n        endpoint: server.get(\"endpoint\").unwrap().as_str().unwrap().to_string(),\n    }\n}\n\nfn channel_symbols_to_command(channel: &str, symbols: &[String], subscribe: bool) -> String {\n    format!(\n        r#\"{{\"id\":\"crypto-ws-client\",\"type\":\"{}\",\"topic\":\"{}:{}\",\"privateChannel\":false,\"response\":true}}\"#,\n        if subscribe { \"subscribe\" } else { \"unsubscribe\" },\n        channel,\n        symbols.join(\",\")\n    )\n}\n\npub(super) fn topics_to_commands(topics: &[(String, String)], subscribe: bool) -> Vec<String> {\n    let mut commands: Vec<String> = Vec::new();\n\n    let mut channel_symbols = BTreeMap::<String, Vec<String>>::new();\n    for (channel, symbol) in topics {\n        match channel_symbols.get_mut(channel) {\n            Some(symbols) => symbols.push(symbol.to_string()),\n            None => {\n                channel_symbols.insert(channel.to_string(), vec![symbol.to_string()]);\n            }\n        }\n    }\n\n    for (channel, symbols) in channel_symbols {\n        let mut chunk: Vec<String> = Vec::new();\n        for symbol in symbols {\n            if chunk.len() >= MAX_TOPICS_PER_COMMAND {\n                commands.push(channel_symbols_to_command(&channel, &chunk, subscribe));\n                chunk.clear();\n            }\n            chunk.push(symbol);\n        }\n        if !chunk.is_empty() {\n            commands.push(channel_symbols_to_command(&channel, &chunk, subscribe));\n        }\n    }\n\n    commands\n}\n\npub(super) struct KucoinMessageHandler {}\n\nimpl MessageHandler for KucoinMessageHandler {\n    fn handle_message(&mut self, msg: &str) -> MiscMessage {\n        let obj = serde_json::from_str::<HashMap<String, Value>>(msg).unwrap();\n        let msg_type = obj.get(\"type\").unwrap().as_str().unwrap();\n        match msg_type {\n            \"pong\" => MiscMessage::Pong,\n            \"welcome\" | \"ack\" => {\n                debug!(\"Received {} from {}\", msg, EXCHANGE_NAME);\n                MiscMessage::Other\n            }\n            \"notice\" | \"command\" => {\n                info!(\"Received {} from {}\", msg, EXCHANGE_NAME);\n                MiscMessage::Other\n            }\n            \"message\" => MiscMessage::Normal,\n            \"error\" => {\n                panic!(\"Received {msg} from {EXCHANGE_NAME}\");\n            }\n            _ => {\n                panic!(\"Received {msg} from {EXCHANGE_NAME}\");\n            }\n        }\n    }\n\n    fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)> {\n        // See:\n        // - https://docs.kucoin.com/#ping\n        // - https://docs.kucoin.cc/futures/#ping\n        //\n        // If the server has not received the ping from the client for 60 seconds , the\n        // connection will be disconnected.\n        Some((Message::Text(r#\"{\"type\":\"ping\", \"id\": \"crypto-ws-client\"}\"#.to_string()), 60))\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn fetch_ws_token() {\n        let ws_token = super::fetch_ws_token().await;\n        assert!(!ws_token.token.is_empty())\n    }\n\n    #[test]\n    fn test_topics_to_commands() {\n        let commands = super::topics_to_commands(\n            &[(\"/market/match\".to_string(), \"BTC-USDT\".to_string())],\n            true,\n        );\n        assert_eq!(1, commands.len());\n    }\n}\n"
  },
  {
    "path": "crypto-ws-client/src/clients/mexc/mexc_spot.rs",
    "content": "use async_trait::async_trait;\nuse std::collections::HashMap;\nuse tokio_tungstenite::tungstenite::Message;\n\nuse super::EXCHANGE_NAME;\nuse crate::{\n    clients::common_traits::{\n        Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO,\n    },\n    common::{\n        command_translator::CommandTranslator,\n        message_handler::{MessageHandler, MiscMessage},\n        ws_client_internal::WSClientInternal,\n    },\n    WSClient,\n};\nuse log::*;\nuse serde_json::Value;\n\npub(super) const SPOT_WEBSOCKET_URL: &str = \"wss://wbs.mexc.com/raw/ws\";\n\n/// MEXC Spot market.\n///\n///   * WebSocket API doc: <https://github.com/mxcdevelop/APIDoc/blob/master/websocket/spot/websocket-api.md>\n///   * Trading at: <https://www.mexc.com/exchange/BTC_USDT>\npub struct MexcSpotWSClient {\n    client: WSClientInternal<MexcMessageHandler>,\n    translator: MexcCommandTranslator,\n}\n\nimpl_new_constructor!(\n    MexcSpotWSClient,\n    EXCHANGE_NAME,\n    SPOT_WEBSOCKET_URL,\n    MexcMessageHandler {},\n    MexcCommandTranslator {}\n);\n\n#[rustfmt::skip]\nimpl_trait!(Trade, MexcSpotWSClient, subscribe_trade, \"deal\");\n#[rustfmt::skip]\nimpl_trait!(OrderBook, MexcSpotWSClient, subscribe_orderbook, \"depth\");\n#[rustfmt::skip]\nimpl_trait!(OrderBookTopK, MexcSpotWSClient, subscribe_orderbook_topk, \"limit.depth\");\nimpl_candlestick!(MexcSpotWSClient);\n\npanic_bbo!(MexcSpotWSClient);\npanic_ticker!(MexcSpotWSClient);\npanic_l3_orderbook!(MexcSpotWSClient);\n\nimpl_ws_client_trait!(MexcSpotWSClient);\n\nstruct MexcMessageHandler {}\nstruct MexcCommandTranslator {}\n\nimpl MessageHandler for MexcMessageHandler {\n    fn handle_message(&mut self, msg: &str) -> MiscMessage {\n        if msg == \"pong\" {\n            return MiscMessage::Pong;\n        }\n        if let Ok(obj) = serde_json::from_str::<HashMap<String, Value>>(msg) {\n            if obj.contains_key(\"channel\") && obj.contains_key(\"data\") {\n                let channel = obj.get(\"channel\").unwrap().as_str().unwrap();\n                match channel {\n                    \"push.deal\" | \"push.depth\" | \"push.limit.depth\" | \"push.kline\" => {\n                        if obj.contains_key(\"symbol\") {\n                            MiscMessage::Normal\n                        } else {\n                            warn!(\"Received {} from {}\", msg, EXCHANGE_NAME);\n                            MiscMessage::Other\n                        }\n                    }\n                    \"push.overview\" => MiscMessage::Normal,\n                    _ => {\n                        warn!(\"Received {} from {}\", msg, EXCHANGE_NAME);\n                        MiscMessage::Other\n                    }\n                }\n            } else {\n                warn!(\"Received {} from {}\", msg, EXCHANGE_NAME);\n                MiscMessage::Other\n            }\n        } else {\n            warn!(\"Received {} from {}\", msg, EXCHANGE_NAME);\n            MiscMessage::Other\n        }\n    }\n\n    fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)> {\n        Some((Message::Text(\"ping\".to_string()), 5))\n    }\n}\n\nimpl MexcCommandTranslator {\n    fn topic_to_command(channel: &str, symbol: &str, subscribe: bool) -> String {\n        if channel == \"limit.depth\" {\n            format!(\n                r#\"{{\"op\":\"{}.{}\",\"symbol\":\"{}\",\"depth\": 5}}\"#,\n                if subscribe { \"sub\" } else { \"unsub\" },\n                channel,\n                symbol\n            )\n        } else {\n            format!(\n                r#\"{{\"op\":\"{}.{}\",\"symbol\":\"{}\"}}\"#,\n                if subscribe { \"sub\" } else { \"unsub\" },\n                channel,\n                symbol\n            )\n        }\n    }\n\n    fn interval_to_string(interval: usize) -> String {\n        let tmp = match interval {\n            60 => \"Min1\",\n            300 => \"Min5\",\n            900 => \"Min15\",\n            1800 => \"Min30\",\n            3600 => \"Min60\",\n            14400 => \"Hour4\",\n            28800 => \"Hour8\",\n            86400 => \"Day1\",\n            604800 => \"Week1\",\n            2592000 => \"Month1\",\n            _ => panic!(\n                \"MEXC has intervals Min1,Min5,Min15,Min30,Min60,Hour4,Hour8,Day1,Week1,Month1\"\n            ),\n        };\n        tmp.to_string()\n    }\n}\n\nimpl CommandTranslator for MexcCommandTranslator {\n    fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec<String> {\n        topics\n            .iter()\n            .map(|(channel, symbol)| {\n                MexcCommandTranslator::topic_to_command(channel, symbol, subscribe)\n            })\n            .collect()\n    }\n\n    fn translate_to_candlestick_commands(\n        &self,\n        subscribe: bool,\n        symbol_interval_list: &[(String, usize)],\n    ) -> Vec<String> {\n        symbol_interval_list\n            .iter()\n            .map(|(symbol, interval)| {\n                format!(\n                    r#\"{{\"op\":\"{}.kline\",\"symbol\":\"{}\",\"interval\":\"{}\"}}\"#,\n                    if subscribe { \"sub\" } else { \"unsub\" },\n                    symbol,\n                    Self::interval_to_string(*interval)\n                )\n            })\n            .collect()\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use crate::common::command_translator::CommandTranslator;\n\n    #[test]\n    fn test_one_topic() {\n        let translator = super::MexcCommandTranslator {};\n        let commands =\n            translator.translate_to_commands(true, &[(\"deal\".to_string(), \"BTC_USDT\".to_string())]);\n\n        assert_eq!(1, commands.len());\n        assert_eq!(r#\"{\"op\":\"sub.deal\",\"symbol\":\"BTC_USDT\"}\"#, commands[0]);\n    }\n\n    #[test]\n    fn test_two_topic() {\n        let translator = super::MexcCommandTranslator {};\n        let commands = translator.translate_to_commands(\n            true,\n            &[\n                (\"deal\".to_string(), \"BTC_USDT\".to_string()),\n                (\"depth\".to_string(), \"ETH_USDT\".to_string()),\n            ],\n        );\n\n        assert_eq!(2, commands.len());\n        assert_eq!(r#\"{\"op\":\"sub.deal\",\"symbol\":\"BTC_USDT\"}\"#, commands[0]);\n        assert_eq!(r#\"{\"op\":\"sub.depth\",\"symbol\":\"ETH_USDT\"}\"#, commands[1]);\n    }\n\n    #[test]\n    fn test_candlestick() {\n        let translator = super::MexcCommandTranslator {};\n        let commands =\n            translator.translate_to_candlestick_commands(true, &[(\"BTC_USDT\".to_string(), 60)]);\n\n        assert_eq!(1, commands.len());\n        assert_eq!(r#\"{\"op\":\"sub.kline\",\"symbol\":\"BTC_USDT\",\"interval\":\"Min1\"}\"#, commands[0]);\n    }\n}\n"
  },
  {
    "path": "crypto-ws-client/src/clients/mexc/mexc_swap.rs",
    "content": "use async_trait::async_trait;\nuse std::collections::HashMap;\nuse tokio_tungstenite::tungstenite::Message;\n\nuse super::EXCHANGE_NAME;\nuse crate::{\n    clients::common_traits::{\n        Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO,\n    },\n    common::{\n        command_translator::CommandTranslator,\n        message_handler::{MessageHandler, MiscMessage},\n        ws_client_internal::WSClientInternal,\n    },\n    WSClient,\n};\n\nuse log::*;\nuse serde_json::Value;\n\npub(super) const SWAP_WEBSOCKET_URL: &str = \"wss://contract.mexc.com/ws\";\n\n/// MEXC Swap market.\n///\n///   * WebSocket API doc: <https://mxcdevelop.github.io/APIDoc/contract.api.en.html#websocket-api>\n///   * Trading at: <https://contract.mexc.com/exchange>\npub struct MexcSwapWSClient {\n    client: WSClientInternal<MexcMessageHandler>,\n    translator: MexcCommandTranslator,\n}\n\nimpl_new_constructor!(\n    MexcSwapWSClient,\n    EXCHANGE_NAME,\n    SWAP_WEBSOCKET_URL,\n    MexcMessageHandler {},\n    MexcCommandTranslator {}\n);\n\n#[rustfmt::skip]\nimpl_trait!(Trade, MexcSwapWSClient, subscribe_trade, \"deal\");\n#[rustfmt::skip]\nimpl_trait!(Ticker, MexcSwapWSClient, subscribe_ticker, \"ticker\");\n#[rustfmt::skip]\nimpl_trait!(OrderBook, MexcSwapWSClient, subscribe_orderbook, \"depth\");\n#[rustfmt::skip]\nimpl_trait!(OrderBookTopK, MexcSwapWSClient, subscribe_orderbook_topk, \"depth.full\");\nimpl_candlestick!(MexcSwapWSClient);\n\npanic_bbo!(MexcSwapWSClient);\npanic_l3_orderbook!(MexcSwapWSClient);\n\nimpl_ws_client_trait!(MexcSwapWSClient);\n\nstruct MexcMessageHandler {}\nstruct MexcCommandTranslator {}\n\nimpl MessageHandler for MexcMessageHandler {\n    fn handle_message(&mut self, msg: &str) -> MiscMessage {\n        let obj = serde_json::from_str::<HashMap<String, Value>>(msg).unwrap();\n        if obj.contains_key(\"channel\") && obj.contains_key(\"data\") && obj.contains_key(\"ts\") {\n            let channel = obj.get(\"channel\").unwrap().as_str().unwrap();\n            match channel {\n                \"pong\" => MiscMessage::Pong,\n                \"rs.error\" => {\n                    error!(\"Received {} from {}\", msg, EXCHANGE_NAME);\n                    panic!(\"Received {msg} from {EXCHANGE_NAME}\");\n                }\n                _ => {\n                    if obj.contains_key(\"symbol\") && channel.starts_with(\"push.\") {\n                        MiscMessage::Normal\n                    } else {\n                        info!(\"Received {} from {}\", msg, EXCHANGE_NAME);\n                        MiscMessage::Other\n                    }\n                }\n            }\n        } else {\n            error!(\"Received {} from {}\", msg, SWAP_WEBSOCKET_URL);\n            MiscMessage::Other\n        }\n    }\n\n    fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)> {\n        // more than 60 seconds no response, close the channel\n        Some((Message::Text(r#\"{\"method\":\"ping\"}\"#.to_string()), 60))\n    }\n}\n\nimpl MexcCommandTranslator {\n    fn topic_to_command(channel: &str, symbol: &str, subscribe: bool) -> String {\n        format!(\n            r#\"{{\"method\":\"{}.{}\",\"param\":{{\"symbol\":\"{}\"}}}}\"#,\n            if subscribe { \"sub\" } else { \"unsub\" },\n            channel,\n            symbol\n        )\n    }\n\n    fn interval_to_string(interval: usize) -> String {\n        let tmp = match interval {\n            60 => \"Min1\",\n            300 => \"Min5\",\n            900 => \"Min15\",\n            1800 => \"Min30\",\n            3600 => \"Min60\",\n            14400 => \"Hour4\",\n            28800 => \"Hour8\",\n            86400 => \"Day1\",\n            604800 => \"Week1\",\n            2592000 => \"Month1\",\n            _ => panic!(\n                \"MEXC has intervals Min1,Min5,Min15,Min30,Min60,Hour4,Hour8,Day1,Week1,Month1\"\n            ),\n        };\n        tmp.to_string()\n    }\n}\n\nimpl CommandTranslator for MexcCommandTranslator {\n    fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec<String> {\n        topics\n            .iter()\n            .map(|(channel, symbol)| {\n                MexcCommandTranslator::topic_to_command(channel, symbol, subscribe)\n            })\n            .collect()\n    }\n\n    fn translate_to_candlestick_commands(\n        &self,\n        subscribe: bool,\n        symbol_interval_list: &[(String, usize)],\n    ) -> Vec<String> {\n        symbol_interval_list\n            .iter()\n            .map(|(symbol, interval)| {\n                format!(\n                    r#\"{{\"method\":\"{}.kline\",\"param\":{{\"symbol\":\"{}\",\"interval\":\"{}\"}}}}\"#,\n                    if subscribe { \"sub\" } else { \"unsub\" },\n                    symbol,\n                    Self::interval_to_string(*interval)\n                )\n            })\n            .collect()\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use crate::common::command_translator::CommandTranslator;\n\n    #[test]\n    fn test_one_topic() {\n        let translator = super::MexcCommandTranslator {};\n        let commands =\n            translator.translate_to_commands(true, &[(\"deal\".to_string(), \"BTC_USDT\".to_string())]);\n\n        assert_eq!(1, commands.len());\n        assert_eq!(r#\"{\"method\":\"sub.deal\",\"param\":{\"symbol\":\"BTC_USDT\"}}\"#, commands[0]);\n    }\n\n    #[test]\n    fn test_two_topic() {\n        let translator = super::MexcCommandTranslator {};\n        let commands = translator.translate_to_commands(\n            true,\n            &[\n                (\"deal\".to_string(), \"BTC_USDT\".to_string()),\n                (\"depth\".to_string(), \"ETH_USDT\".to_string()),\n            ],\n        );\n\n        assert_eq!(2, commands.len());\n        assert_eq!(r#\"{\"method\":\"sub.deal\",\"param\":{\"symbol\":\"BTC_USDT\"}}\"#, commands[0]);\n        assert_eq!(r#\"{\"method\":\"sub.depth\",\"param\":{\"symbol\":\"ETH_USDT\"}}\"#, commands[1]);\n    }\n\n    #[test]\n    fn test_candlestick() {\n        let translator = super::MexcCommandTranslator {};\n        let commands =\n            translator.translate_to_candlestick_commands(true, &[(\"BTC_USDT\".to_string(), 60)]);\n\n        assert_eq!(1, commands.len());\n        assert_eq!(\n            r#\"{\"method\":\"sub.kline\",\"param\":{\"symbol\":\"BTC_USDT\",\"interval\":\"Min1\"}}\"#,\n            commands[0]\n        );\n    }\n}\n"
  },
  {
    "path": "crypto-ws-client/src/clients/mexc/mod.rs",
    "content": "mod mexc_spot;\nmod mexc_swap;\n\npub(super) const EXCHANGE_NAME: &str = \"mexc\";\n\npub use mexc_spot::MexcSpotWSClient;\npub use mexc_swap::MexcSwapWSClient;\n"
  },
  {
    "path": "crypto-ws-client/src/clients/mod.rs",
    "content": "#[macro_use]\npub(super) mod common_traits;\n\npub(super) mod binance;\npub(super) mod binance_option;\npub(super) mod bitfinex;\npub(super) mod bitget;\npub(super) mod bithumb;\npub(super) mod bitmex;\npub(super) mod bitstamp;\npub(super) mod bitz;\npub(super) mod bybit;\npub(super) mod coinbase_pro;\npub(super) mod deribit;\npub(super) mod dydx;\npub(super) mod ftx;\npub(super) mod gate;\npub(super) mod huobi;\npub(super) mod kraken;\npub(super) mod kucoin;\npub(super) mod mexc;\npub(super) mod okx;\npub(super) mod zb;\npub(super) mod zbg;\n"
  },
  {
    "path": "crypto-ws-client/src/clients/okx.rs",
    "content": "use async_trait::async_trait;\nuse nonzero_ext::nonzero;\nuse std::{\n    collections::{BTreeMap, HashMap},\n    num::NonZeroU32,\n};\nuse tokio_tungstenite::tungstenite::Message;\n\nuse log::*;\nuse serde_json::Value;\n\nuse crate::{\n    clients::common_traits::{\n        Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO,\n    },\n    common::{\n        command_translator::CommandTranslator,\n        message_handler::{MessageHandler, MiscMessage},\n        utils::ensure_frame_size,\n        ws_client_internal::WSClientInternal,\n    },\n    WSClient,\n};\n\npub(crate) const EXCHANGE_NAME: &str = \"okx\";\n\nconst WEBSOCKET_URL: &str = \"wss://ws.okx.com:8443/ws/v5/public\";\n\n/// https://www.okx.com/docs-v5/en/#websocket-api-subscribe\n/// The total length of multiple channels cannot exceed 4096 bytes\nconst WS_FRAME_SIZE: usize = 4096;\n\n// Subscription limit: 240 times per hour\n// see https://www.okx.com/docs-v5/en/#websocket-api-connect\nconst UPLINK_LIMIT: (NonZeroU32, std::time::Duration) =\n    (nonzero!(240u32), std::time::Duration::from_secs(3600));\n\n/// The WebSocket client for OKX.\n///\n/// OKX has Spot, Future, Swap and Option markets.\n///\n/// * API doc: <https://www.okx.com/docs-v5/en/#websocket-api>\n/// * Trading at:\n///     * Spot <https://www.okx.com/trade-spot>\n///     * Future <https://www.okx.com/trade-futures>\n///     * Swap <https://www.okx.com/trade-swap>\n///     * Option <https://www.okx.com/trade-option>\npub struct OkxWSClient {\n    client: WSClientInternal<OkxMessageHandler>,\n    translator: OkxCommandTranslator,\n}\n\nimpl OkxWSClient {\n    pub async fn new(tx: std::sync::mpsc::Sender<String>, url: Option<&str>) -> Self {\n        let real_url = match url {\n            Some(endpoint) => endpoint,\n            None => WEBSOCKET_URL,\n        };\n        OkxWSClient {\n            client: WSClientInternal::connect(\n                EXCHANGE_NAME,\n                real_url,\n                OkxMessageHandler {},\n                Some(UPLINK_LIMIT),\n                tx,\n            )\n            .await,\n            translator: OkxCommandTranslator {},\n        }\n    }\n}\n\nimpl_trait!(Trade, OkxWSClient, subscribe_trade, \"trades\");\nimpl_trait!(Ticker, OkxWSClient, subscribe_ticker, \"tickers\");\nimpl_trait!(BBO, OkxWSClient, subscribe_bbo, \"bbo-tbt\");\n#[rustfmt::skip]\n// books-l2-tbt and books50-l2-tbt require login, only books doesn't require it\nimpl_trait!(OrderBook, OkxWSClient, subscribe_orderbook, \"books\");\n#[rustfmt::skip]\nimpl_trait!(OrderBookTopK, OkxWSClient, subscribe_orderbook_topk, \"books5\");\nimpl_candlestick!(OkxWSClient);\npanic_l3_orderbook!(OkxWSClient);\n\nimpl_ws_client_trait!(OkxWSClient);\n\nstruct OkxMessageHandler {}\nstruct OkxCommandTranslator {}\n\nimpl OkxCommandTranslator {\n    fn topics_to_command(chunk: &[(String, String)], subscribe: bool) -> String {\n        let arr = chunk\n            .iter()\n            .map(|t| {\n                let mut map = BTreeMap::new();\n                let (channel, symbol) = t;\n                map.insert(\"channel\".to_string(), channel.to_string());\n                map.insert(\"instId\".to_string(), symbol.to_string());\n                map\n            })\n            .collect::<Vec<BTreeMap<String, String>>>();\n        format!(\n            r#\"{{\"op\":\"{}\",\"args\":{}}}\"#,\n            if subscribe { \"subscribe\" } else { \"unsubscribe\" },\n            serde_json::to_string(&arr).unwrap(),\n        )\n    }\n\n    // see https://www.okx.com/docs-v5/en/#websocket-api-public-channel-candlesticks-channel\n    fn to_candlestick_raw_channel(interval: usize) -> &'static str {\n        match interval {\n            60 => \"candle1m\",\n            180 => \"candle3m\",\n            300 => \"candle5m\",\n            900 => \"candle15m\",\n            1800 => \"candle30m\",\n            3600 => \"candle1H\",\n            7200 => \"candle2H\",\n            14400 => \"candle4H\",\n            21600 => \"candle6H\",\n            43200 => \"candle12H\",\n            86400 => \"candle1D\",\n            172800 => \"candle2D\",\n            259200 => \"candle3D\",\n            432000 => \"candle5D\",\n            604800 => \"candle1W\",\n            2592000 => \"candle1M\",\n            _ => panic!(\"Invalid OKX candlestick interval {interval}\"),\n        }\n    }\n}\n\nimpl MessageHandler for OkxMessageHandler {\n    fn handle_message(&mut self, msg: &str) -> MiscMessage {\n        if msg == \"pong\" {\n            return MiscMessage::Pong;\n        }\n        let resp = serde_json::from_str::<HashMap<String, Value>>(msg);\n        if resp.is_err() {\n            error!(\"{} is not a JSON string, {}\", msg, EXCHANGE_NAME);\n            return MiscMessage::Other;\n        }\n        let obj = resp.unwrap();\n\n        if let Some(event) = obj.get(\"event\") {\n            match event.as_str().unwrap() {\n                \"error\" => {\n                    let error_code =\n                        obj.get(\"code\").unwrap().as_str().unwrap().parse::<i64>().unwrap();\n                    match error_code {\n                        30040 => {\n                            // channel doesn't exist, ignore because some symbols don't exist in\n                            // websocket while they exist in `/v3/instruments`\n                            error!(\"Received {} from {}\", msg, EXCHANGE_NAME);\n                        }\n                        _ => panic!(\"Received {msg} from {EXCHANGE_NAME}\"),\n                    }\n                }\n                \"subscribe\" => info!(\"Received {} from {}\", msg, EXCHANGE_NAME),\n                \"unsubscribe\" => info!(\"Received {} from {}\", msg, EXCHANGE_NAME),\n                _ => warn!(\"Received {} from {}\", msg, EXCHANGE_NAME),\n            }\n            MiscMessage::Other\n        } else if !obj.contains_key(\"arg\") || !obj.contains_key(\"data\") {\n            error!(\"Received {} from {}\", msg, EXCHANGE_NAME);\n            MiscMessage::Other\n        } else {\n            MiscMessage::Normal\n        }\n    }\n\n    fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)> {\n        // https://www.okx.com/docs-v5/en/#websocket-api-connect\n        Some((Message::Text(\"ping\".to_string()), 30))\n    }\n}\n\nimpl CommandTranslator for OkxCommandTranslator {\n    fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec<String> {\n        ensure_frame_size(topics, subscribe, Self::topics_to_command, WS_FRAME_SIZE, None)\n    }\n\n    fn translate_to_candlestick_commands(\n        &self,\n        subscribe: bool,\n        symbol_interval_list: &[(String, usize)],\n    ) -> Vec<String> {\n        let topics = symbol_interval_list\n            .iter()\n            .map(|(symbol, interval)| {\n                let channel = Self::to_candlestick_raw_channel(*interval);\n                (channel.to_string(), symbol.to_string())\n            })\n            .collect::<Vec<(String, String)>>();\n        self.translate_to_commands(subscribe, &topics)\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use crate::common::command_translator::CommandTranslator;\n\n    #[test]\n    fn test_one_topic() {\n        let translator = super::OkxCommandTranslator {};\n        let commands = translator\n            .translate_to_commands(true, &[(\"trades\".to_string(), \"BTC-USDT\".to_string())]);\n\n        assert_eq!(1, commands.len());\n        assert_eq!(\n            r#\"{\"op\":\"subscribe\",\"args\":[{\"channel\":\"trades\",\"instId\":\"BTC-USDT\"}]}\"#,\n            commands[0]\n        );\n    }\n\n    #[test]\n    fn test_two_topics() {\n        let translator = super::OkxCommandTranslator {};\n        let commands = translator.translate_to_commands(\n            true,\n            &[\n                (\"trades\".to_string(), \"BTC-USDT\".to_string()),\n                (\"tickers\".to_string(), \"BTC-USDT\".to_string()),\n            ],\n        );\n\n        assert_eq!(1, commands.len());\n        assert_eq!(\n            r#\"{\"op\":\"subscribe\",\"args\":[{\"channel\":\"trades\",\"instId\":\"BTC-USDT\"},{\"channel\":\"tickers\",\"instId\":\"BTC-USDT\"}]}\"#,\n            commands[0]\n        );\n    }\n}\n"
  },
  {
    "path": "crypto-ws-client/src/clients/zb/mod.rs",
    "content": "mod zb_spot;\nmod zb_swap;\n\npub use zb_spot::ZbSpotWSClient;\npub use zb_swap::ZbSwapWSClient;\n\nconst EXCHANGE_NAME: &str = \"zb\";\n"
  },
  {
    "path": "crypto-ws-client/src/clients/zb/zb_spot.rs",
    "content": "use std::collections::HashMap;\n\nuse async_trait::async_trait;\nuse log::*;\nuse serde_json::Value;\nuse tokio_tungstenite::tungstenite::Message;\n\nuse crate::{\n    clients::common_traits::{\n        Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO,\n    },\n    common::{\n        command_translator::CommandTranslator,\n        message_handler::{MessageHandler, MiscMessage},\n        ws_client_internal::WSClientInternal,\n    },\n    WSClient,\n};\n\nuse super::EXCHANGE_NAME;\n\n// If you're in China, use wss://api.zbex.site/websocket instead\nconst WEBSOCKET_URL: &str = \"wss://api.zb.com/websocket\";\n\n/// The WebSocket client for ZB spot market.\n///\n/// * WebSocket API doc: <https://www.zb.com/en/api>\n/// * Trading at: <https://www.zb.com/en/kline/btc_usdt>\npub struct ZbSpotWSClient {\n    client: WSClientInternal<ZbMessageHandler>,\n    translator: ZbCommandTranslator,\n}\n\nimpl_new_constructor!(\n    ZbSpotWSClient,\n    EXCHANGE_NAME,\n    WEBSOCKET_URL,\n    ZbMessageHandler {},\n    ZbCommandTranslator {}\n);\n\n#[rustfmt::skip]\nimpl_trait!(Trade, ZbSpotWSClient, subscribe_trade, \"trades\");\n#[rustfmt::skip]\nimpl_trait!(OrderBookTopK, ZbSpotWSClient, subscribe_orderbook_topk, \"depth\");\n#[rustfmt::skip]\nimpl_trait!(Ticker, ZbSpotWSClient, subscribe_ticker, \"ticker\");\nimpl_candlestick!(ZbSpotWSClient);\n\npanic_bbo!(ZbSpotWSClient);\npanic_l2!(ZbSpotWSClient);\npanic_l3_orderbook!(ZbSpotWSClient);\n\nimpl_ws_client_trait!(ZbSpotWSClient);\n\nstruct ZbMessageHandler {}\nstruct ZbCommandTranslator {}\n\nimpl MessageHandler for ZbMessageHandler {\n    fn handle_message(&mut self, msg: &str) -> MiscMessage {\n        let obj = serde_json::from_str::<HashMap<String, Value>>(msg).unwrap();\n        let channel = obj[\"channel\"].as_str().unwrap();\n\n        if channel == \"pong\" {\n            return MiscMessage::Pong;\n        }\n        if let Some(code) = obj.get(\"code\") {\n            let code = code.as_i64().unwrap();\n            if code != 1000 {\n                if code == 1007 {\n                    panic!(\"Received {msg} from {EXCHANGE_NAME}\");\n                } else {\n                    error!(\"Received {} from {}\", msg, EXCHANGE_NAME);\n                }\n                return MiscMessage::Other;\n            }\n        }\n        MiscMessage::Normal\n    }\n\n    fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)> {\n        Some((Message::Text(r#\"{\"channel\":\"ping\",\"event\":\"addChannel\"}\"#.to_string()), 3))\n    }\n}\n\nimpl ZbCommandTranslator {\n    fn to_candlestick_raw_channel(&self, symbol: &str, interval: usize) -> String {\n        let interval_str = match interval {\n            60 => \"1min\",\n            180 => \"3min\",\n            300 => \"5min\",\n            900 => \"15min\",\n            1800 => \"30min\",\n            3600 => \"1hour\",\n            7200 => \"2hour\",\n            14400 => \"4hour\",\n            21600 => \"6hour\",\n            43200 => \"12hour\",\n            86400 => \"1day\",\n            259200 => \"3day\",\n            604800 => \"1week\",\n            _ => panic!(\n                \"ZB spot available intervals: 1week, 3day, 1day, 12hour, 6hour, 4hour, 2hour, 1hour, 30min, 15min, 5min, 3min, 1min\"\n            ),\n        };\n        format!(\"{}_kline_{}\", symbol.replace('_', \"\"), interval_str,)\n    }\n}\n\nimpl CommandTranslator for ZbCommandTranslator {\n    fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec<String> {\n        topics\n            .iter()\n            .map(|(channel, symbol)| {\n                format!(\n                    r#\"{{\"event\":\"{}\",\"channel\":\"{}_{}\"}}\"#,\n                    if subscribe { \"addChannel\" } else { \"removeChannel\" },\n                    symbol.replace('_', \"\"),\n                    channel,\n                )\n            })\n            .collect()\n    }\n\n    fn translate_to_candlestick_commands(\n        &self,\n        subscribe: bool,\n        symbol_interval_list: &[(String, usize)],\n    ) -> Vec<String> {\n        symbol_interval_list\n            .iter()\n            .map(|(symbol, interval)| {\n                format!(\n                    r#\"{{\"event\":\"{}\",\"channel\":\"{}\"}}\"#,\n                    if subscribe { \"addChannel\" } else { \"removeChannel\" },\n                    self.to_candlestick_raw_channel(symbol, *interval),\n                )\n            })\n            .collect()\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use crate::common::command_translator::CommandTranslator;\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn test_one_topic() {\n        let translator = super::ZbCommandTranslator {};\n        let commands = translator\n            .translate_to_commands(true, &[(\"trades\".to_string(), \"btc_usdt\".to_string())]);\n\n        assert_eq!(1, commands.len());\n        assert_eq!(r#\"{\"event\":\"addChannel\",\"channel\":\"btcusdt_trades\"}\"#, commands[0]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn test_two_topic() {\n        let translator = super::ZbCommandTranslator {};\n        let commands = translator.translate_to_commands(\n            true,\n            &[\n                (\"trades\".to_string(), \"btc_usdt\".to_string()),\n                (\"depth\".to_string(), \"eth_usdt\".to_string()),\n            ],\n        );\n\n        assert_eq!(2, commands.len());\n        assert_eq!(r#\"{\"event\":\"addChannel\",\"channel\":\"btcusdt_trades\"}\"#, commands[0]);\n        assert_eq!(r#\"{\"event\":\"addChannel\",\"channel\":\"ethusdt_depth\"}\"#, commands[1]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn test_candlestick() {\n        let translator = super::ZbCommandTranslator {};\n        let commands =\n            translator.translate_to_candlestick_commands(true, &[(\"btc_usdt\".to_string(), 60)]);\n\n        assert_eq!(1, commands.len());\n        assert_eq!(r#\"{\"event\":\"addChannel\",\"channel\":\"btcusdt_kline_1min\"}\"#, commands[0]);\n    }\n}\n"
  },
  {
    "path": "crypto-ws-client/src/clients/zb/zb_swap.rs",
    "content": "use std::{collections::HashMap, num::NonZeroU32};\n\nuse async_trait::async_trait;\nuse nonzero_ext::nonzero;\nuse serde_json::Value;\nuse tokio_tungstenite::tungstenite::Message;\n\nuse crate::{\n    clients::common_traits::{\n        Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO,\n    },\n    common::{\n        command_translator::CommandTranslator,\n        message_handler::{MessageHandler, MiscMessage},\n        ws_client_internal::WSClientInternal,\n    },\n    WSClient,\n};\nuse log::*;\n\nuse super::EXCHANGE_NAME;\n\nconst WEBSOCKET_URL: &str = \"wss://fapi.zb.com/ws/public/v1\";\n\n// The default limit for the number of requests for a single interface is 200\n// times/2s\n//\n// See https://github.com/ZBFuture/docs/blob/main/API%20V2%20_en.md#14-access-limit-frequency-rules\nconst UPLINK_LIMIT: (NonZeroU32, std::time::Duration) =\n    (nonzero!(200u32), std::time::Duration::from_secs(2));\n\n/// The WebSocket client for ZB swap market.\n///\n/// * WebSocket API doc: <https://github.com/ZBFuture/docs/blob/main/API%20V2%20_en.md>\n/// * Trading at: <https://www.zb.com/en/futures/btc_usdt>\npub struct ZbSwapWSClient {\n    client: WSClientInternal<ZbMessageHandler>,\n    translator: ZbCommandTranslator,\n}\n\nimpl ZbSwapWSClient {\n    pub async fn new(tx: std::sync::mpsc::Sender<String>, url: Option<&str>) -> Self {\n        let real_url = match url {\n            Some(endpoint) => endpoint,\n            None => WEBSOCKET_URL,\n        };\n        ZbSwapWSClient {\n            client: WSClientInternal::connect(\n                EXCHANGE_NAME,\n                real_url,\n                ZbMessageHandler {},\n                Some(UPLINK_LIMIT),\n                tx,\n            )\n            .await,\n            translator: ZbCommandTranslator {},\n        }\n    }\n}\n\n#[rustfmt::skip]\nimpl_trait!(Trade, ZbSwapWSClient, subscribe_trade, \"Trade\");\n#[rustfmt::skip]\nimpl_trait!(OrderBook, ZbSwapWSClient, subscribe_orderbook, \"Depth\");\n#[rustfmt::skip]\nimpl_trait!(OrderBookTopK, ZbSwapWSClient, subscribe_orderbook_topk, \"DepthWhole\");\n#[rustfmt::skip]\nimpl_trait!(Ticker, ZbSwapWSClient, subscribe_ticker, \"Ticker\");\nimpl_candlestick!(ZbSwapWSClient);\n\npanic_bbo!(ZbSwapWSClient);\npanic_l3_orderbook!(ZbSwapWSClient);\n\nimpl_ws_client_trait!(ZbSwapWSClient);\n\nstruct ZbMessageHandler {}\nstruct ZbCommandTranslator {}\n\nimpl MessageHandler for ZbMessageHandler {\n    fn handle_message(&mut self, msg: &str) -> MiscMessage {\n        if msg == r#\"{\"action\":\"pong\"}\"# {\n            return MiscMessage::Pong;\n        }\n        if msg.contains(\"error\") {\n            error!(\"Received {} from {}\", msg, EXCHANGE_NAME);\n            return MiscMessage::Other;\n        }\n        let obj = serde_json::from_str::<HashMap<String, Value>>(msg).unwrap();\n        if obj.contains_key(\"channel\") && obj.contains_key(\"data\") {\n            MiscMessage::Normal\n        } else {\n            warn!(\"Received {} from {}\", msg, EXCHANGE_NAME);\n            MiscMessage::Other\n        }\n    }\n\n    fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)> {\n        // https://github.com/ZBFuture/docs/blob/main/API%20V2%20_en.md#812-ping\n        Some((Message::Text(r#\"{\"action\":\"ping\"}\"#.to_string()), 10))\n    }\n}\n\nimpl ZbCommandTranslator {\n    fn to_candlestick_raw_channel(&self, symbol: &str, interval: usize) -> String {\n        let interval_str = match interval {\n            60 => \"1M\",\n            300 => \"5M\",\n            900 => \"15M\",\n            1800 => \"30M\",\n            3600 => \"1H\",\n            21600 => \"6H\",\n            86400 => \"1D\",\n            432000 => \"5D\",\n            _ => panic!(\"ZB swap available intervals: 1M,5M,15M, 30M, 1H, 6H, 1D, 5D\"),\n        };\n        format!(\"{symbol}.KLine_{interval_str}\",)\n    }\n}\n\nimpl CommandTranslator for ZbCommandTranslator {\n    fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec<String> {\n        let action = if subscribe { \"subscribe\" } else { \"unsubscribe\" };\n        topics\n            .iter()\n            .map(|(channel, symbol)| match channel.as_str() {\n                \"Trade\" => format!(\n                    r#\"{{\"action\":\"{action}\", \"channel\":\"{symbol}.{channel}\", \"size\":100}}\"#,\n                ),\n                \"Depth\" => format!(\n                    r#\"{{\"action\":\"{action}\", \"channel\":\"{symbol}.{channel}\", \"size\":200}}\"#,\n                ),\n                \"DepthWhole\" => format!(\n                    r#\"{{\"action\":\"{action}\", \"channel\":\"{symbol}.{channel}\", \"size\":10}}\"#,\n                ),\n                \"Ticker\" => {\n                    format!(r#\"{{\"action\":\"{action}\", \"channel\":\"{symbol}.{channel}\"}}\"#,)\n                }\n                _ => panic!(\"Unknown ZB channel {channel}\"),\n            })\n            .collect()\n    }\n\n    fn translate_to_candlestick_commands(\n        &self,\n        subscribe: bool,\n        symbol_interval_list: &[(String, usize)],\n    ) -> Vec<String> {\n        let action = if subscribe { \"subscribe\" } else { \"unsubscribe\" };\n        symbol_interval_list\n            .iter()\n            .map(|(symbol, interval)| {\n                format!(\n                    r#\"{{\"action\":\"{}\", \"channel\":\"{}\", \"size\":1}}\"#,\n                    action,\n                    self.to_candlestick_raw_channel(symbol, *interval),\n                )\n            })\n            .collect()\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use crate::common::command_translator::CommandTranslator;\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn test_one_topic() {\n        let translator = super::ZbCommandTranslator {};\n        let commands = translator\n            .translate_to_commands(true, &[(\"Trade\".to_string(), \"BTC_USDT\".to_string())]);\n\n        assert_eq!(1, commands.len());\n        assert_eq!(\n            r#\"{\"action\":\"subscribe\", \"channel\":\"BTC_USDT.Trade\", \"size\":100}\"#,\n            commands[0]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn test_two_topic() {\n        let translator = super::ZbCommandTranslator {};\n        let commands = translator.translate_to_commands(\n            true,\n            &[\n                (\"Trade\".to_string(), \"BTC_USDT\".to_string()),\n                (\"Depth\".to_string(), \"ETH_USDT\".to_string()),\n            ],\n        );\n\n        assert_eq!(2, commands.len());\n        assert_eq!(\n            r#\"{\"action\":\"subscribe\", \"channel\":\"BTC_USDT.Trade\", \"size\":100}\"#,\n            commands[0]\n        );\n        assert_eq!(\n            r#\"{\"action\":\"subscribe\", \"channel\":\"ETH_USDT.Depth\", \"size\":200}\"#,\n            commands[1]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn test_candlestick() {\n        let translator = super::ZbCommandTranslator {};\n        let commands =\n            translator.translate_to_candlestick_commands(true, &[(\"BTC_USDT\".to_string(), 60)]);\n\n        assert_eq!(1, commands.len());\n        assert_eq!(\n            r#\"{\"action\":\"subscribe\", \"channel\":\"BTC_USDT.KLine_1M\", \"size\":1}\"#,\n            commands[0]\n        );\n    }\n}\n"
  },
  {
    "path": "crypto-ws-client/src/clients/zbg/mod.rs",
    "content": "mod utils;\nmod zbg_spot;\nmod zbg_swap;\n\npub use zbg_spot::ZbgSpotWSClient;\npub use zbg_swap::ZbgSwapWSClient;\n\nconst EXCHANGE_NAME: &str = \"zbg\";\n"
  },
  {
    "path": "crypto-ws-client/src/clients/zbg/utils.rs",
    "content": "use std::collections::HashMap;\n\nuse reqwest::{header, Result};\nuse serde_json::Value;\n\nasync fn http_get(url: &str) -> Result<String> {\n    let mut headers = header::HeaderMap::new();\n    headers.insert(header::CONTENT_TYPE, header::HeaderValue::from_static(\"application/json\"));\n\n    let client = reqwest::Client::builder()\n         .default_headers(headers)\n         .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\")\n         .gzip(true)\n         .build()?;\n    let response = client.get(url).send().await?;\n\n    match response.error_for_status() {\n        Ok(resp) => Ok(resp.text().await?),\n        Err(error) => Err(error),\n    }\n}\n\n// See https://zbgapi.github.io/docs/spot/v1/en/#public-get-all-supported-trading-symbols\npub(super) async fn fetch_symbol_id_map_spot() -> HashMap<String, i64> {\n    let mut symbol_id_map: HashMap<String, i64> =\n        vec![(\"btc_usdt\", 329), (\"eth_usdt\", 330), (\"eos_usdt\", 333), (\"zb_usdt\", 321)]\n            .into_iter()\n            .map(|x| (x.0.to_string(), x.1))\n            .collect();\n\n    if let Ok(txt) = http_get(\"https://www.zbg.com/exchange/api/v1/common/symbols\").await {\n        if let Ok(obj) = serde_json::from_str::<HashMap<String, Value>>(&txt) {\n            if obj\n                .get(\"resMsg\")\n                .unwrap()\n                .as_object()\n                .unwrap()\n                .get(\"code\")\n                .unwrap()\n                .as_str()\n                .unwrap()\n                == \"1\"\n            {\n                let arr = obj.get(\"datas\").unwrap().as_array().unwrap();\n                for v in arr.iter() {\n                    let obj = v.as_object().unwrap();\n                    let symbol = obj.get(\"symbol\").unwrap().as_str().unwrap();\n                    let id = obj.get(\"id\").unwrap().as_str().unwrap();\n\n                    symbol_id_map.insert(symbol.to_string(), id.parse::<i64>().unwrap());\n                }\n            }\n        }\n    }\n\n    symbol_id_map\n}\n\n// See https://zbgapi.github.io/docs/future/v1/en/#public-get-contracts\npub(super) async fn fetch_symbol_contract_id_map_swap() -> HashMap<String, i64> {\n    let mut symbol_contract_id_map: HashMap<String, i64> = vec![\n        (\"BTC_USDT\", 1000000),\n        (\"BTC_USD-R\", 1000001),\n        (\"ETH_USDT\", 1000002),\n        (\"ETH_USD-R\", 1000003),\n    ]\n    .into_iter()\n    .map(|x| (x.0.to_string(), x.1))\n    .collect();\n\n    if let Ok(txt) = http_get(\"https://www.zbg.com/exchange/api/v1/future/common/contracts\").await {\n        if let Ok(obj) = serde_json::from_str::<HashMap<String, Value>>(&txt) {\n            if obj\n                .get(\"resMsg\")\n                .unwrap()\n                .as_object()\n                .unwrap()\n                .get(\"code\")\n                .unwrap()\n                .as_str()\n                .unwrap()\n                == \"1\"\n            {\n                let arr = obj.get(\"datas\").unwrap().as_array().unwrap();\n                for v in arr.iter() {\n                    let obj = v.as_object().unwrap();\n                    let symbol = obj.get(\"symbol\").unwrap().as_str().unwrap();\n                    let contract_id = obj.get(\"contractId\").unwrap().as_i64().unwrap();\n\n                    symbol_contract_id_map.insert(symbol.to_string(), contract_id);\n                }\n            }\n        }\n    }\n\n    symbol_contract_id_map\n}\n"
  },
  {
    "path": "crypto-ws-client/src/clients/zbg/zbg_spot.rs",
    "content": "use async_trait::async_trait;\nuse std::collections::HashMap;\nuse tokio_tungstenite::tungstenite::Message;\n\nuse crate::{\n    clients::common_traits::{\n        Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO,\n    },\n    common::{\n        command_translator::CommandTranslator,\n        message_handler::{MessageHandler, MiscMessage},\n        ws_client_internal::WSClientInternal,\n    },\n    WSClient,\n};\n\nuse super::{utils::fetch_symbol_id_map_spot, EXCHANGE_NAME};\n\nconst WEBSOCKET_URL: &str = \"wss://kline.zbg.com/websocket\";\n\n/// The WebSocket client for ZBG spot market.\n///\n/// * WebSocket API doc: <https://www.zbg.com/docs/spot/v1/en/#websocket-market-data>\n/// * Trading at: <https://www.zbg.com/trade/>\npub struct ZbgSpotWSClient {\n    client: WSClientInternal<ZbgMessageHandler>,\n    translator: ZbgCommandTranslator,\n}\n\nimpl_new_constructor!(\n    ZbgSpotWSClient,\n    EXCHANGE_NAME,\n    WEBSOCKET_URL,\n    ZbgMessageHandler {},\n    ZbgCommandTranslator::new().await\n);\n\n#[rustfmt::skip]\nimpl_trait!(Trade, ZbgSpotWSClient, subscribe_trade, \"TRADE\");\n#[rustfmt::skip]\nimpl_trait!(OrderBook, ZbgSpotWSClient, subscribe_orderbook, \"ENTRUST_ADD\");\n#[rustfmt::skip]\nimpl_trait!(Ticker, ZbgSpotWSClient, subscribe_ticker, \"TRADE_STATISTIC_24H\");\nimpl_candlestick!(ZbgSpotWSClient);\n\npanic_bbo!(ZbgSpotWSClient);\npanic_l2_topk!(ZbgSpotWSClient);\npanic_l3_orderbook!(ZbgSpotWSClient);\n\nimpl_ws_client_trait!(ZbgSpotWSClient);\n\nstruct ZbgMessageHandler {}\nstruct ZbgCommandTranslator {\n    symbol_id_map: HashMap<String, i64>,\n}\n\nimpl MessageHandler for ZbgMessageHandler {\n    fn handle_message(&mut self, msg: &str) -> MiscMessage {\n        if msg.contains(r#\"action\":\"PING\"#) { MiscMessage::Pong } else { MiscMessage::Normal }\n    }\n\n    fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)> {\n        Some((Message::Text(r#\"{\"action\":\"PING\"}\"#.to_string()), 10))\n    }\n}\n\nimpl ZbgCommandTranslator {\n    async fn new() -> Self {\n        let symbol_id_map = fetch_symbol_id_map_spot().await;\n        ZbgCommandTranslator { symbol_id_map }\n    }\n\n    fn to_raw_channel(&self, channel: &str, symbol: &str) -> String {\n        let symbol_id = self\n            .symbol_id_map\n            .get(symbol.to_lowercase().as_str())\n            .unwrap_or_else(|| panic!(\"Failed to find symbol_id for {symbol}\"));\n        if channel == \"TRADE_STATISTIC_24H\" {\n            format!(\"{symbol_id}_{channel}\")\n        } else {\n            format!(\"{}_{}_{}\", symbol_id, channel, symbol.to_uppercase())\n        }\n    }\n\n    fn to_candlestick_raw_channel(&self, symbol: &str, interval: usize) -> String {\n        let interval_str = match interval {\n            60 => \"1M\",\n            300 => \"5M\",\n            900 => \"15M\",\n            1800 => \"30M\",\n            3600 => \"1H\",\n            14400 => \"4H\",\n            86400 => \"1D\",\n            604800 => \"1W\",\n            _ => panic!(\"ZBG spot available intervals 1M,5M,15M,30M,1H,4H,1D,1W\"),\n        };\n\n        let symbol_id = self\n            .symbol_id_map\n            .get(symbol.to_lowercase().as_str())\n            .unwrap_or_else(|| panic!(\"Failed to find symbol_id for {symbol}\"));\n\n        format!(\"{}_KLINE_{}_{}\", symbol_id, interval_str, symbol.to_uppercase())\n    }\n}\n\nimpl CommandTranslator for ZbgCommandTranslator {\n    fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec<String> {\n        topics\n            .iter()\n            .map(|(channel, symbol)| {\n                format!(\n                    r#\"{{\"action\":\"{}\", \"dataType\":{}}}\"#,\n                    if subscribe { \"ADD\" } else { \"DEL\" },\n                    self.to_raw_channel(channel, symbol),\n                )\n            })\n            .collect()\n    }\n\n    fn translate_to_candlestick_commands(\n        &self,\n        subscribe: bool,\n        symbol_interval_list: &[(String, usize)],\n    ) -> Vec<String> {\n        symbol_interval_list\n            .iter()\n            .map(|(symbol, interval)| {\n                format!(\n                    r#\"{{\"action\":\"{}\", \"dataType\":{}}}\"#,\n                    if subscribe { \"ADD\" } else { \"DEL\" },\n                    self.to_candlestick_raw_channel(symbol, *interval),\n                )\n            })\n            .collect()\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use crate::common::command_translator::CommandTranslator;\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn test_one_topic() {\n        let translator = super::ZbgCommandTranslator::new().await;\n        let commands = translator\n            .translate_to_commands(true, &[(\"TRADE\".to_string(), \"btc_usdt\".to_string())]);\n\n        assert_eq!(1, commands.len());\n        assert_eq!(r#\"{\"action\":\"ADD\", \"dataType\":329_TRADE_BTC_USDT}\"#, commands[0]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn test_two_topic() {\n        let translator = super::ZbgCommandTranslator::new().await;\n        let commands = translator.translate_to_commands(\n            true,\n            &[\n                (\"TRADE\".to_string(), \"btc_usdt\".to_string()),\n                (\"ENTRUST_ADD\".to_string(), \"eth_usdt\".to_string()),\n            ],\n        );\n\n        assert_eq!(2, commands.len());\n        assert_eq!(r#\"{\"action\":\"ADD\", \"dataType\":329_TRADE_BTC_USDT}\"#, commands[0]);\n        assert_eq!(r#\"{\"action\":\"ADD\", \"dataType\":330_ENTRUST_ADD_ETH_USDT}\"#, commands[1]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn test_candlestick() {\n        let translator = super::ZbgCommandTranslator::new().await;\n        let commands =\n            translator.translate_to_candlestick_commands(true, &[(\"btc_usdt\".to_string(), 60)]);\n\n        assert_eq!(1, commands.len());\n        assert_eq!(r#\"{\"action\":\"ADD\", \"dataType\":329_KLINE_1M_BTC_USDT}\"#, commands[0]);\n    }\n}\n"
  },
  {
    "path": "crypto-ws-client/src/clients/zbg/zbg_swap.rs",
    "content": "use async_trait::async_trait;\nuse std::collections::HashMap;\nuse tokio_tungstenite::tungstenite::Message;\n\nuse crate::{\n    clients::common_traits::{\n        Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO,\n    },\n    common::{\n        command_translator::CommandTranslator,\n        message_handler::{MessageHandler, MiscMessage},\n        ws_client_internal::WSClientInternal,\n    },\n    WSClient,\n};\n\nuse log::*;\n\nuse super::{utils::fetch_symbol_contract_id_map_swap, EXCHANGE_NAME};\n\nconst WEBSOCKET_URL: &str = \"wss://kline.zbg.com/exchange/v1/futurews\";\n\n/// The WebSocket client for ZBG swap market.\n///\n/// * WebSocket API doc: <https://www.zbgpro.com/docs/future/v1/cn/#300f34d976>,\n///   there is no English doc\n/// * Trading at: <https://futures.zbg.com/>\npub struct ZbgSwapWSClient {\n    client: WSClientInternal<ZbgMessageHandler>,\n    translator: ZbgCommandTranslator,\n}\n\nimpl_new_constructor!(\n    ZbgSwapWSClient,\n    EXCHANGE_NAME,\n    WEBSOCKET_URL,\n    ZbgMessageHandler {},\n    ZbgCommandTranslator::new().await\n);\n\n#[rustfmt::skip]\nimpl_trait!(Trade, ZbgSwapWSClient, subscribe_trade, \"future_tick\");\n#[rustfmt::skip]\nimpl_trait!(OrderBook, ZbgSwapWSClient, subscribe_orderbook, \"future_snapshot_depth\");\n#[rustfmt::skip]\nimpl_trait!(Ticker, ZbgSwapWSClient, subscribe_ticker, \"future_snapshot_indicator\");\nimpl_candlestick!(ZbgSwapWSClient);\n\npanic_bbo!(ZbgSwapWSClient);\npanic_l2_topk!(ZbgSwapWSClient);\npanic_l3_orderbook!(ZbgSwapWSClient);\n\nimpl_ws_client_trait!(ZbgSwapWSClient);\n\nstruct ZbgMessageHandler {}\nstruct ZbgCommandTranslator {\n    symbol_id_map: HashMap<String, i64>,\n}\n\nimpl MessageHandler for ZbgMessageHandler {\n    fn handle_message(&mut self, msg: &str) -> MiscMessage {\n        if msg == \"Pong\" {\n            return MiscMessage::Pong;\n        }\n        if msg.starts_with('[') {\n            MiscMessage::Normal\n        } else {\n            warn!(\"Received {} from {}\", msg, EXCHANGE_NAME);\n            MiscMessage::Other\n        }\n    }\n\n    fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)> {\n        Some((Message::Text(\"PING\".to_string()), 25))\n    }\n}\n\nimpl ZbgCommandTranslator {\n    async fn new() -> Self {\n        let symbol_id_map = fetch_symbol_contract_id_map_swap().await;\n        ZbgCommandTranslator { symbol_id_map }\n    }\n\n    fn to_raw_channel(&self, channel: &str, symbol: &str) -> String {\n        let contract_id = self\n            .symbol_id_map\n            .get(symbol)\n            .unwrap_or_else(|| panic!(\"Failed to find contract_id for {symbol}\"));\n        format!(\"{channel}-{contract_id}\")\n    }\n\n    fn to_candlestick_raw_channel(&self, pair: &str, interval: usize) -> String {\n        let valid_set: Vec<usize> =\n            vec![60, 180, 300, 900, 1800, 3600, 7200, 14400, 21600, 43200, 86400, 604800];\n        if !valid_set.contains(&interval) {\n            let joined =\n                valid_set.into_iter().map(|x| x.to_string()).collect::<Vec<String>>().join(\",\");\n            panic!(\"ZBG Swap available intervals {joined}\");\n        }\n\n        let contract_id = self\n            .symbol_id_map\n            .get(pair)\n            .unwrap_or_else(|| panic!(\"Failed to find contract_id for {pair}\"));\n\n        format!(\"future_kline-{}-{}\", contract_id, interval * 1000)\n    }\n}\n\nimpl CommandTranslator for ZbgCommandTranslator {\n    fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec<String> {\n        topics\n            .iter()\n            .map(|(channel, symbol)| {\n                format!(\n                    r#\"{{\"action\":\"{}\", \"topic\":\"{}\"}}\"#,\n                    if subscribe { \"sub\" } else { \"unsub\" },\n                    self.to_raw_channel(channel, symbol),\n                )\n            })\n            .collect()\n    }\n\n    fn translate_to_candlestick_commands(\n        &self,\n        subscribe: bool,\n        symbol_interval_list: &[(String, usize)],\n    ) -> Vec<String> {\n        symbol_interval_list\n            .iter()\n            .map(|(symbol, interval)| {\n                format!(\n                    r#\"{{\"action\":\"{}\", \"topic\":\"{}\"}}\"#,\n                    if subscribe { \"sub\" } else { \"unsub\" },\n                    self.to_candlestick_raw_channel(symbol, *interval),\n                )\n            })\n            .collect()\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use crate::common::command_translator::CommandTranslator;\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn test_one_topic() {\n        let translator = super::ZbgCommandTranslator::new().await;\n        let commands = translator\n            .translate_to_commands(true, &[(\"future_tick\".to_string(), \"BTC_USDT\".to_string())]);\n\n        assert_eq!(1, commands.len());\n        assert_eq!(r#\"{\"action\":\"sub\", \"topic\":\"future_tick-1000000\"}\"#, commands[0]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn test_two_topic() {\n        let translator = super::ZbgCommandTranslator::new().await;\n        let commands = translator.translate_to_commands(\n            true,\n            &[\n                (\"future_tick\".to_string(), \"BTC_USDT\".to_string()),\n                (\"future_snapshot_depth\".to_string(), \"ETH_USDT\".to_string()),\n            ],\n        );\n\n        assert_eq!(2, commands.len());\n        assert_eq!(r#\"{\"action\":\"sub\", \"topic\":\"future_tick-1000000\"}\"#, commands[0]);\n        assert_eq!(r#\"{\"action\":\"sub\", \"topic\":\"future_snapshot_depth-1000002\"}\"#, commands[1]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn test_candlestick() {\n        let translator = super::ZbgCommandTranslator::new().await;\n        let commands =\n            translator.translate_to_candlestick_commands(true, &[(\"BTC_USDT\".to_string(), 60)]);\n\n        assert_eq!(1, commands.len());\n        assert_eq!(r#\"{\"action\":\"sub\", \"topic\":\"future_kline-1000000-60000\"}\"#, commands[0]);\n    }\n}\n"
  },
  {
    "path": "crypto-ws-client/src/common/command_translator.rs",
    "content": "/// Translate to exchange-specific websocket subscribe/unsubscribe commands.\n///\n/// topic = channel + symbol\n///\n/// A command is a JSON string which can be aceepted by the websocket server,\n/// and every exchange has its own format.\npub(crate) trait CommandTranslator {\n    fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec<String>;\n    fn translate_to_candlestick_commands(\n        &self,\n        subscribe: bool,\n        symbol_interval_list: &[(String, usize)],\n    ) -> Vec<String>;\n}\n"
  },
  {
    "path": "crypto-ws-client/src/common/connect_async.rs",
    "content": "use fast_socks5::client::{Config, Socks5Stream};\nuse futures_util::{SinkExt, StreamExt};\nuse governor::{Quota, RateLimiter};\nuse log::*;\nuse nonzero_ext::*;\nuse reqwest::Url;\nuse std::{env, num::NonZeroU32};\nuse tokio::{\n    io::{AsyncRead, AsyncWrite},\n    sync::mpsc::{Receiver, Sender},\n};\nuse tokio_tungstenite::{\n    tungstenite::{Error, Message},\n    MaybeTlsStream, WebSocketStream,\n};\n\n/// Wraps a websocket client inside an event loop, returns a message_rx to\n/// receive messages and a command_tx to send commands to the websocket server.\n///\n/// To close the websocket connection, send a `Message::Close` message to the\n/// command_tx.\n///\n/// `limit`, max number of uplink messsages, for example, 100 per 10 seconds\npub async fn connect_async(\n    url: &str,\n    uplink_limit: Option<(NonZeroU32, std::time::Duration)>,\n) -> Result<(Receiver<Message>, Sender<Message>), Error> {\n    if let Ok(proxy_env) = env::var(\"https_proxy\").or_else(|_| env::var(\"http_proxy\")) {\n        let proxy_url = Url::parse(&proxy_env).unwrap();\n        let proxy_scheme = proxy_url.scheme().to_lowercase();\n        if proxy_scheme.as_str() != \"socks5\" {\n            panic!(\"Unsupported proxy scheme {proxy_scheme}\");\n        }\n        let proxy_addr = format!(\n            \"{}:{}\",\n            proxy_url.host_str().unwrap(),\n            proxy_url.port_or_known_default().unwrap()\n        );\n        let connect_url = Url::parse(url).unwrap();\n        let proxy_stream = Socks5Stream::connect(\n            proxy_addr.to_string(),\n            connect_url.host_str().unwrap().to_string(),\n            connect_url.port_or_known_default().unwrap(),\n            Config::default(),\n        )\n        .await\n        .unwrap();\n        let (ws_stream, _) = tokio_tungstenite::client_async_tls(connect_url, proxy_stream).await?;\n        // replaced\n        // let ret = tokio_tungstenite::connect_async(url).await;\n        connect_async_internal(ws_stream, uplink_limit).await\n    } else {\n        let (ws_stream, _) = tokio_tungstenite::connect_async(url).await?;\n\n        connect_async_internal(ws_stream, uplink_limit).await\n    }\n}\n\nasync fn connect_async_internal<S: AsyncRead + AsyncWrite + Unpin + Send + 'static>(\n    ws_stream: WebSocketStream<MaybeTlsStream<S>>,\n    uplink_limit: Option<(NonZeroU32, std::time::Duration)>,\n) -> Result<(Receiver<Message>, Sender<Message>), Error> {\n    let (command_tx, mut command_rx) = tokio::sync::mpsc::channel::<Message>(1);\n    let (message_tx, message_rx) = tokio::sync::mpsc::channel::<Message>(32);\n\n    let (mut write, mut read) = ws_stream.split();\n\n    let limiter = if let Some((max_burst, duration)) = uplink_limit {\n        let quota = Quota::with_period(duration).unwrap().allow_burst(max_burst);\n        RateLimiter::direct(quota)\n    } else {\n        RateLimiter::direct(Quota::per_second(nonzero!(u32::max_value())))\n    };\n\n    tokio::task::spawn(async move {\n        loop {\n            tokio::select! {\n              command = command_rx.recv() => {\n                match command {\n                  Some(command) => {\n                    match command {\n                      Message::Close(resp) => {\n                        match resp {\n                            Some(frame) => {\n                                warn!(\n                                    \"Received a CloseFrame: code: {}, reason: {}\",\n                                    frame.code, frame.reason\n                                );\n                            }\n                            None => warn!(\"Received an empty close message\"),\n                        }\n                        break; // close the connection and break the loop\n                      }\n                      _ => {\n                        limiter.until_ready().await;\n                        if let Err(err) =write.send(command).await {\n                          error!(\"Failed to send, error: {}\", err);\n                        }\n                      }\n                    }\n                  }\n                  None => {\n                    debug!(\"command_rx closed\");\n                    break;\n                  }\n                }\n              }\n              msg = read.next() => match msg {\n                Some(Ok(msg)) => {\n                  let _= message_tx.send(msg).await;\n                }\n                Some(Err(err)) => {\n                  error!(\"Failed to read, error: {}\", err);\n                  break;\n                }\n                None => {\n                  debug!(\"message_tx closed\");\n                  break;\n                }\n              }\n            };\n        }\n        _ = write.send(Message::Close(None)).await;\n    });\n\n    Ok((message_rx, command_tx))\n}\n"
  },
  {
    "path": "crypto-ws-client/src/common/message_handler.rs",
    "content": "use tokio_tungstenite::tungstenite::Message;\n\n#[derive(Debug)]\npub(crate) enum MiscMessage {\n    Normal,             // A normal websocket message which contains a JSON string\n    Mutated(String),    // A JSON string mutated by a handler, e.g., bitfinex\n    WebSocket(Message), // WebSocket message that needs to be sent to the server\n    Pong,               // Pong message from the server\n    Reconnect,          // Needs to reconnect\n    Other,              // Other messages will be ignored\n}\n\n/// Exchange-specific message handler.\npub(crate) trait MessageHandler {\n    /// Given a message from the exchange, return a MiscMessage which will be\n    /// procesed in run().\n    fn handle_message(&mut self, msg: &str) -> MiscMessage;\n    /// To keep the connection alive, how often should the client send a ping?\n    /// None means the client doesn't need to send ping, instead the server will\n    /// send ping and the client just needs to reply a pong\n    fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)>;\n}\n"
  },
  {
    "path": "crypto-ws-client/src/common/mod.rs",
    "content": "pub(crate) mod command_translator;\npub(crate) mod connect_async;\npub(crate) mod message_handler;\npub(super) mod utils;\npub(crate) mod ws_client;\npub(super) mod ws_client_internal;\n"
  },
  {
    "path": "crypto-ws-client/src/common/utils.rs",
    "content": "/// Ensure that length of a websocket message does not exceed the max size or\n/// the number of topics does not exceed the threshold.\npub(crate) fn ensure_frame_size(\n    topics: &[(String, String)],\n    subscribe: bool,\n    topics_to_command: fn(&[(String, String)], bool) -> String,\n    max_bytes: usize,\n    max_topics_per_command: Option<usize>,\n) -> Vec<String> {\n    let mut all_commands: Vec<String> = Vec::new();\n\n    let mut begin = 0;\n    while begin < topics.len() {\n        for end in (begin + 1)..(topics.len() + 1) {\n            let num_subscriptions = end - begin;\n            let chunk = &topics[begin..end];\n            let command = topics_to_command(chunk, subscribe);\n            if end == topics.len() {\n                all_commands.push(command);\n                begin = end;\n            } else if num_subscriptions >= max_topics_per_command.unwrap_or(usize::MAX) {\n                all_commands.push(command);\n                begin = end;\n                break;\n            } else {\n                let chunk = &topics[begin..end + 1];\n                let command_next = topics_to_command(chunk, subscribe);\n                if command_next.len() > max_bytes {\n                    all_commands.push(command);\n                    begin = end;\n                    break;\n                }\n            };\n        }\n    }\n\n    all_commands\n}\n\npub(crate) fn topic_to_raw_channel(topic: &(String, String)) -> String {\n    topic.0.replace(\"SYMBOL\", topic.1.as_str())\n}\n"
  },
  {
    "path": "crypto-ws-client/src/common/ws_client.rs",
    "content": "use async_trait::async_trait;\n\n/// The public interface of every WebSocket client.\n#[async_trait]\npub trait WSClient {\n    /// Subscribes to trade channels.\n    ///\n    /// A trade channel sends tick-by-tick trade data,  which is the complete\n    /// copy of a market's trading information.\n    ///\n    /// Each exchange has its own symbol formats, for example:\n    ///\n    /// * BitMEX `XBTUSD`, `XBTM21`\n    /// * Binance `btcusdt`, `btcusd_perp`\n    /// * OKEx `BTC-USDT`\n    async fn subscribe_trade(&self, symbols: &[String]);\n\n    /// Subscribes to BBO(best bid & offer) channels.\n    ///\n    /// BBO represents best bid and offer, which is also refered to as level1\n    /// data. It is the top 1 bid and ask from the orginal orderbook, so BBO\n    /// is updated per tick and non-aggregated.\n    ///\n    /// Not all exchanges have the BBO channel, calling this function with\n    /// these exchanges will panic.\n    ///\n    /// * Binance, BitMEX, Huobi and Kraken have BBO directly.\n    /// * Bitfinex uses `book` channel with `len=1` and `prec=\"R0\"` to get BBO\n    ///   data.\n    async fn subscribe_bbo(&self, symbols: &[String]);\n\n    /// Subscribes to incremental level2 orderbook channels.\n    ///\n    /// An incremental level2 orderbook channel sends a snapshot followed by\n    /// tick-by-tick updates.\n    ///\n    /// Level2 orderbook is the raw orderbook(Level3) aggregated by price level,\n    /// it is also refered to as \"market by price level\" data.\n    ///\n    /// This function subscribes to exchange specific channels as the following:\n    ///\n    /// * Binance `depth`\n    /// * Bitfinex `book` channel with `prec=P0`, `frec=F0` and `len=25`\n    /// * BitMEX `orderBookL2_25`\n    /// * Bitstamp `diff_order_book`, top 100\n    /// * CoinbasePro `level2`\n    /// * Huobi `depth.size_20.high_freq` with `data_type=incremental` for\n    ///   contracts, `mbp.20` for Spot\n    /// * Kraken `book` with `depth=25`\n    /// * MEXC `depth` for Swap, `symbol` for Spot\n    /// * OKEx `depth_l2_tbt`, top 100\n    async fn subscribe_orderbook(&self, symbols: &[String]);\n\n    /// Subscribes to level2 orderbook snapshot channels.\n    ///\n    /// A level2 orderbook snapshot channel sends a complete snapshot every\n    /// interval.\n    ///\n    /// This function subscribes to exchange specific channels as the following:\n    ///\n    /// * Binance `depth5`, every 1000ms\n    /// * Bitfinex has no snapshot channel\n    /// * BitMEX `orderBook10`, top 10, every tick\n    /// * Bitstamp `order_book`, top 10, every 100ms\n    /// * CoinbasePro has no snapshot channel\n    /// * Huobi `depth.step1` and `depth.step7`, top 20, every 1s\n    /// * Kraken has no snapshot channel\n    /// * MEXC `depth.full` for Swap, top 20, every 100ms; `get.depth` for Spot,\n    ///   full, every 26s\n    /// * OKEx `depth5`, top 5, every 100ms\n    async fn subscribe_orderbook_topk(&self, symbols: &[String]);\n\n    /// Subscribes to level3 orderebook channels.\n    ///\n    /// **Only bitfinex, bitstamp, coinbase_pro and kucoin have level3 orderbook\n    /// channels.**\n    ///\n    /// The level3 orderbook is the orginal orderbook of an exchange, it is\n    /// non-aggregated by price level and updated tick-by-tick.\n    async fn subscribe_l3_orderbook(&self, symbols: &[String]);\n\n    /// Subscribes to ticker channels.\n    ///\n    /// A ticker channel pushes realtime 24hr rolling window ticker messages,\n    /// which contains OHLCV information.\n    ///\n    /// Not all exchanges have the ticker channel, for example, BitMEX,\n    /// Bitstamp, MEXC Spot, etc.\n    async fn subscribe_ticker(&self, symbols: &[String]);\n\n    /// Subscribes to candlestick channels.\n    ///\n    /// The candlestick channel sends OHLCV messages at interval.\n    ///\n    /// `symbol_interval_list` is a list of symbols and intervals of\n    /// candlesticks in seconds.\n    ///\n    /// Not all exchanges have candlestick channels, for example, Bitstamp\n    /// and CoinbasePro.\n    async fn subscribe_candlestick(&self, symbol_interval_list: &[(String, usize)]);\n\n    /// Subscribe to multiple topics.\n    ///\n    /// topic = channel + symbol, a topic will be converted to an\n    /// exchange-specific channel(called `raw channel`).\n    ///\n    /// The channel string supports `SYMBOL` as a placeholder, for example,\n    /// `(\"market.SYMBOL.trade.detail\", \"btcusdt\")` will be converted to a raw\n    /// channel `\"market.btcusdt.trade.detail\"`.\n    ///\n    /// Examples:\n    ///\n    /// * Binance: `vec![(\"aggTrade\".to_string(),\n    ///   \"BTCUSDT\".to_string()),(\"ticker\".to_string(), \"BTCUSDT\".to_string())]`\n    /// * Deribit: `vec![(trades.SYMBOL.100ms\".to_string(),\"BTC-PERPETUAL\".\n    ///   to_string()),(trades.SYMBOL.100ms\".to_string(),\"ETH-PERPETUAL\".\n    ///   to_string())]`\n    /// * Huobi: `vec![(\"trade.detail\".to_string(),\n    ///   \"btcusdt\".to_string()),(\"trade.detail\".to_string(),\n    ///   \"ethusdt\".to_string())]`\n    /// * OKX: `vec![(\"trades\".to_string(),\n    ///   \"BTC-USDT\".to_string()),(\"trades\".to_string(),\n    ///   \"ETH-USDT\".to_string())]`\n    async fn subscribe(&self, topics: &[(String, String)]);\n\n    /// Unsubscribes multiple topics.\n    ///\n    /// topic = channel + symbol\n    async fn unsubscribe(&self, topics: &[(String, String)]);\n\n    /// Send raw JSON commands.\n    ///\n    /// This is a low-level API for advanced users only.\n    async fn send(&self, commands: &[String]);\n\n    /// Starts the infinite event loop.\n    async fn run(&self);\n\n    /// Close the connection and break the loop in Run().\n    async fn close(&self);\n}\n"
  },
  {
    "path": "crypto-ws-client/src/common/ws_client_internal.rs",
    "content": "use std::{\n    io::prelude::*,\n    num::NonZeroU32,\n    sync::{\n        atomic::{AtomicIsize, Ordering},\n        Arc,\n    },\n    time::Duration,\n};\n\nuse flate2::read::{DeflateDecoder, GzDecoder};\nuse log::*;\nuse reqwest::StatusCode;\nuse tokio_tungstenite::tungstenite::{Error, Message};\n\nuse crate::common::message_handler::{MessageHandler, MiscMessage};\n\n// `WSClientInternal` should be Sync + Send so that it can be put into Arc\n// directly.\npub(crate) struct WSClientInternal<H: MessageHandler> {\n    exchange: &'static str, // Eexchange name\n    pub(crate) url: String, // Websocket base url\n    // pass parameters to run()\n    #[allow(clippy::type_complexity)]\n    params_rx: std::sync::Mutex<\n        tokio::sync::oneshot::Receiver<(\n            H,\n            tokio::sync::mpsc::Receiver<Message>,\n            std::sync::mpsc::Sender<String>,\n        )>,\n    >,\n    command_tx: tokio::sync::mpsc::Sender<Message>,\n}\n\nimpl<H: MessageHandler> WSClientInternal<H> {\n    pub async fn connect(\n        exchange: &'static str,\n        url: &str,\n        handler: H,\n        uplink_limit: Option<(NonZeroU32, std::time::Duration)>,\n        tx: std::sync::mpsc::Sender<String>,\n    ) -> Self {\n        // A channel to send parameters to run()\n        let (params_tx, params_rx) = tokio::sync::oneshot::channel::<(\n            H,\n            tokio::sync::mpsc::Receiver<Message>,\n            std::sync::mpsc::Sender<String>,\n        )>();\n\n        match super::connect_async::connect_async(url, uplink_limit).await {\n            Ok((message_rx, command_tx)) => {\n                let _ = params_tx.send((handler, message_rx, tx));\n\n                WSClientInternal {\n                    exchange,\n                    url: url.to_string(),\n                    params_rx: std::sync::Mutex::new(params_rx),\n                    command_tx,\n                }\n            }\n            Err(err) => match err {\n                Error::Http(resp) => {\n                    if resp.status() == StatusCode::TOO_MANY_REQUESTS {\n                        if let Some(retry_after) = resp.headers().get(\"retry-after\") {\n                            let mut seconds = retry_after.to_str().unwrap().parse::<u64>().unwrap();\n                            seconds += rand::random::<u64>() % 9 + 1; // add random seconds to avoid concurrent requests\n                            error!(\n                                \"The retry-after header value is {}, sleeping for {} seconds now\",\n                                retry_after.to_str().unwrap(),\n                                seconds\n                            );\n                            tokio::time::sleep(Duration::from_secs(seconds)).await;\n                        }\n                    }\n                    panic!(\"Failed to connect to {url} due to 429 too many requests\")\n                }\n                _ => panic!(\"Failed to connect to {url}, error: {err}\"),\n            },\n        }\n    }\n\n    pub async fn send(&self, commands: &[String]) {\n        for command in commands {\n            debug!(\"{}\", command);\n            if self.command_tx.send(Message::Text(command.to_string())).await.is_err() {\n                break; // break the loop if there is no receiver\n            }\n        }\n    }\n\n    pub async fn run(&self) {\n        let (mut handler, mut message_rx, tx) = {\n            let mut guard = self.params_rx.lock().unwrap();\n            guard.try_recv().unwrap()\n        };\n\n        let num_unanswered_ping = Arc::new(AtomicIsize::new(0)); // for debug only\n        if let Some((msg, interval)) = handler.get_ping_msg_and_interval() {\n            // send heartbeat periodically\n            let command_tx_clone = self.command_tx.clone();\n            let num_unanswered_ping_clone = num_unanswered_ping.clone();\n            tokio::task::spawn(async move {\n                let mut timer = {\n                    let duration = Duration::from_secs(interval / 2 + 1);\n                    tokio::time::interval(duration)\n                };\n                loop {\n                    let now = timer.tick().await;\n                    debug!(\"{:?} sending ping {}\", now, msg.to_text().unwrap());\n                    if let Err(err) = command_tx_clone.send(msg.clone()).await {\n                        error!(\"Error sending ping {}\", err);\n                    } else {\n                        num_unanswered_ping_clone.fetch_add(1, Ordering::SeqCst);\n                    }\n                }\n            });\n        }\n\n        while let Some(msg) = message_rx.recv().await {\n            let txt = match msg {\n                Message::Text(txt) => Some(txt),\n                Message::Binary(binary) => {\n                    let mut txt = String::new();\n                    let resp = match self.exchange {\n                        crate::clients::huobi::EXCHANGE_NAME\n                        | crate::clients::binance::EXCHANGE_NAME\n                        | \"bitget\"\n                        | \"bitz\" => {\n                            let mut decoder = GzDecoder::new(&binary[..]);\n                            decoder.read_to_string(&mut txt)\n                        }\n                        crate::clients::okx::EXCHANGE_NAME => {\n                            let mut decoder = DeflateDecoder::new(&binary[..]);\n                            decoder.read_to_string(&mut txt)\n                        }\n                        _ => {\n                            panic!(\"Unknown binary format from {}\", self.url);\n                        }\n                    };\n\n                    match resp {\n                        Ok(_) => Some(txt),\n                        Err(err) => {\n                            error!(\"Decompression failed, {}\", err);\n                            None\n                        }\n                    }\n                }\n                Message::Ping(resp) => {\n                    // binance server will send a ping frame every 3 or 5 minutes\n                    debug!(\n                        \"Received a ping frame: {} from {}\",\n                        std::str::from_utf8(&resp).unwrap(),\n                        self.url,\n                    );\n                    if self.exchange == \"binance\" {\n                        // send a pong frame\n                        debug!(\"Sending a pong frame to {}\", self.url);\n                        _ = self.command_tx.send(Message::Pong(Vec::new())).await;\n                    }\n                    None\n                }\n                Message::Pong(resp) => {\n                    num_unanswered_ping.store(0, Ordering::Release);\n                    debug!(\n                        \"Received a pong frame: {} from {}, reset num_unanswered_ping to {}\",\n                        std::str::from_utf8(&resp).unwrap(),\n                        self.exchange,\n                        num_unanswered_ping.load(Ordering::Acquire)\n                    );\n                    None\n                }\n                Message::Frame(_) => todo!(),\n                Message::Close(resp) => {\n                    match resp {\n                        Some(frame) => {\n                            warn!(\n                                \"Received a CloseFrame: code: {}, reason: {} from {}\",\n                                frame.code, frame.reason, self.url\n                            );\n                        }\n                        None => warn!(\"Received a close message without CloseFrame\"),\n                    }\n                    // break;\n                    panic!(\"Received a CloseFrame\"); //fail fast so that pm2 can restart the process\n                }\n            };\n\n            if let Some(txt) = txt {\n                let txt = txt.as_str().trim().to_string();\n                match handler.handle_message(&txt) {\n                    MiscMessage::Normal => {\n                        // the receiver might get dropped earlier than this loop\n                        if tx.send(txt).is_err() {\n                            break; // break the loop if there is no receiver\n                        }\n                    }\n                    MiscMessage::Mutated(txt) => _ = tx.send(txt),\n                    MiscMessage::WebSocket(ws_msg) => _ = self.command_tx.send(ws_msg).await,\n                    MiscMessage::Pong => {\n                        num_unanswered_ping.store(0, Ordering::Release);\n                        debug!(\n                            \"Received {} from {}, reset num_unanswered_ping to {}\",\n                            txt,\n                            self.exchange,\n                            num_unanswered_ping.load(Ordering::Acquire)\n                        );\n                    }\n                    MiscMessage::Reconnect => break, /* fail fast, pm2 will restart, restart is */\n                    // reconnect\n                    MiscMessage::Other => (), // ignore\n                }\n            }\n        }\n    }\n\n    pub async fn close(&self) {\n        // close the websocket connection and break the while loop in run()\n        _ = self.command_tx.send(Message::Close(None)).await;\n    }\n}\n"
  },
  {
    "path": "crypto-ws-client/src/lib.rs",
    "content": "//! A versatile websocket client that supports many cryptocurrency exchanges.\n//!\n//! ## Example\n//!\n//! ```\n//! use crypto_ws_client::{BinanceSpotWSClient, WSClient};\n//!\n//! #[tokio::main]\n//! async fn main() {\n//!     let (tx, rx) = std::sync::mpsc::channel();\n//!     tokio::task::spawn(async move {\n//!         let symbols = vec![\"BTCUSDT\".to_string(), \"ETHUSDT\".to_string()];\n//!         let ws_client = BinanceSpotWSClient::new(tx, None).await;\n//!         ws_client.subscribe_trade(&symbols).await;\n//!         // run for 5 seconds\n//!         let _ = tokio::time::timeout(std::time::Duration::from_secs(5), ws_client.run()).await;\n//!         ws_client.close();\n//!     });\n//!\n//!     let mut messages = Vec::new();\n//!     for msg in rx {\n//!         messages.push(msg);\n//!     }\n//!     assert!(!messages.is_empty());\n//! }\n//! ```\n//! ## High Level APIs\n//!\n//! The following APIs are high-level APIs with ease of use:\n//!\n//! * `subscribe_trade(&self, symbols: &[String])`\n//! * `subscribe_bbo(&self, symbols: &[String])`\n//! * `subscribe_orderbook(&self, symbols: &[String])`\n//! * `subscribe_ticker(&self, symbols: &[String])`\n//! * `subscribe_candlestick(&self, symbol_interval_list: &[(String, usize)])`\n//!\n//! They are easier to use and cover most user scenarios.\n//!\n//! ## Low Level APIs\n//!\n//! Sometimes high-level APIs can NOT meet users' requirements, this package\n//! provides three low-level APIs:\n//!\n//! * `subscribe(&self, topics: &[(String, String)])`\n//! * `unsubscribe(&self, topics: &[(String, String)])`\n//! * `send(&self, commands: &[String])`\n//!\n//! ## OrderBook Data Categories\n//!\n//! Each orderbook has three properties: `aggregation`, `frequency` and `depth`.\n//!\n//! |                      | Aggregated        | Non-Aggregated |\n//! | -------------------- | ----------------- | -------------- |\n//! | Updated per Tick     | Inremental Level2 | Level3         |\n//! | Updated per Interval | Snapshot Level2   | Not Usefull    |\n//!\n//! * Level1 data is non-aggregated, updated per tick, top 1 bid & ask from the\n//!   original orderbook.\n//! * Level2 data is aggregated by price level, updated per tick.\n//! * Level3 data is the original orderbook, which is not aggregated.\n\nmod clients;\nmod common;\n\npub use common::ws_client::WSClient;\n\npub use clients::{\n    binance::*, binance_option::*, bitfinex::*, bitget::*, bithumb::*, bitmex::*, bitstamp::*,\n    bitz::*, bybit::*, coinbase_pro::*, deribit::*, dydx::*, ftx::*, gate::*, huobi::*, kraken::*,\n    kucoin::*, mexc::*, okx::*, zb::*, zbg::*,\n};\n"
  },
  {
    "path": "crypto-ws-client/tests/binance.rs",
    "content": "#[macro_use]\nmod utils;\n\n#[cfg(test)]\nmod binance_spot {\n    use crypto_ws_client::{BinanceSpotWSClient, WSClient};\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe() {\n        gen_test_code!(\n            BinanceSpotWSClient,\n            subscribe,\n            &[\n                (\"aggTrade\".to_string(), \"BTCUSDT\".to_string()),\n                (\"ticker\".to_string(), \"BTCUSDT\".to_string())\n            ]\n        );\n    }\n\n    #[ignore = \"!bookTicker has been removed since December 7, 2022\"]\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_all_bbo() {\n        gen_test_code!(\n            BinanceSpotWSClient,\n            send,\n            &[r#\"{\"id\":9527,\"method\":\"SUBSCRIBE\",\"params\":[\"!bookTicker\"]}\"#.to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_raw_json() {\n        gen_test_code!(\n            BinanceSpotWSClient,\n            send,\n            &[r#\"{\"id\":9527,\"method\":\"SUBSCRIBE\",\"params\":[\"btcusdt@aggTrade\",\"btcusdt@ticker\"]}\"#\n                .to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_trade() {\n        gen_test_code!(\n            BinanceSpotWSClient,\n            subscribe_trade,\n            &[\"BTCUSDT\".to_string(), \"ETHUSDT\".to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_ticker() {\n        gen_test_code!(\n            BinanceSpotWSClient,\n            subscribe_ticker,\n            &[\"BTCUSDT\".to_string(), \"ETHUSDT\".to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_tickers_all() {\n        gen_test_code!(\n            BinanceSpotWSClient,\n            send,\n            &[r#\"{\"id\":9527,\"method\":\"SUBSCRIBE\",\"params\":[\"!ticker@arr\"]}\"#.to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_bbo() {\n        gen_test_code!(\n            BinanceSpotWSClient,\n            subscribe_bbo,\n            &[\"BTCUSDT\".to_string(), \"ETHUSDT\".to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook() {\n        gen_test_code!(\n            BinanceSpotWSClient,\n            subscribe_orderbook,\n            &[\"BTCUSDT\".to_string(), \"ETHUSDT\".to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook_topk() {\n        gen_test_code!(\n            BinanceSpotWSClient,\n            subscribe_orderbook_topk,\n            &[\"BTCUSDT\".to_string(), \"ETHUSDT\".to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_candlestick() {\n        gen_test_subscribe_candlestick!(\n            BinanceSpotWSClient,\n            &[(\"BTCUSDT\".to_string(), 60), (\"ETHUSDT\".to_string(), 60)]\n        );\n        gen_test_subscribe_candlestick!(\n            BinanceSpotWSClient,\n            &[(\"BTCUSDT\".to_string(), 2592000), (\"ETHUSDT\".to_string(), 2592000)]\n        );\n    }\n}\n\n#[cfg(test)]\nmod binance_inverse_future {\n    use crypto_ws_client::{BinanceInverseWSClient, WSClient};\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe() {\n        gen_test_code!(\n            BinanceInverseWSClient,\n            subscribe,\n            &[\n                (\"aggTrade\".to_string(), \"BTCUSD_221230\".to_string()),\n                (\"aggTrade\".to_string(), \"ETHUSD_221230\".to_string()),\n                (\"aggTrade\".to_string(), \"BNBUSD_221230\".to_string())\n            ]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_all_bbo() {\n        gen_test_code!(\n            BinanceInverseWSClient,\n            send,\n            &[r#\"{\"id\":9527,\"method\":\"SUBSCRIBE\",\"params\":[\"!bookTicker\"]}\"#.to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_trade() {\n        gen_test_code!(\n            BinanceInverseWSClient,\n            subscribe_trade,\n            &[\n                \"BTCUSD_221230\".to_string(),\n                \"ETHUSD_221230\".to_string(),\n                \"BNBUSD_221230\".to_string()\n            ]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_ticker() {\n        gen_test_code!(\n            BinanceInverseWSClient,\n            subscribe_ticker,\n            &[\n                \"BTCUSD_221230\".to_string(),\n                \"ETHUSD_221230\".to_string(),\n                \"BNBUSD_221230\".to_string()\n            ]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_tickers_all() {\n        gen_test_code!(\n            BinanceInverseWSClient,\n            send,\n            &[r#\"{\"id\":9527,\"method\":\"SUBSCRIBE\",\"params\":[\"!ticker@arr\"]}\"#.to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_bbo() {\n        gen_test_code!(\n            BinanceInverseWSClient,\n            subscribe_bbo,\n            &[\n                \"BTCUSD_221230\".to_string(),\n                \"ETHUSD_221230\".to_string(),\n                \"BNBUSD_221230\".to_string()\n            ]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook() {\n        gen_test_code!(\n            BinanceInverseWSClient,\n            subscribe_orderbook,\n            &[\n                \"BTCUSD_221230\".to_string(),\n                \"ETHUSD_221230\".to_string(),\n                \"BNBUSD_221230\".to_string()\n            ]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook_topk() {\n        gen_test_code!(\n            BinanceInverseWSClient,\n            subscribe_orderbook_topk,\n            &[\n                \"BTCUSD_221230\".to_string(),\n                \"ETHUSD_221230\".to_string(),\n                \"BNBUSD_221230\".to_string()\n            ]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_candlestick() {\n        gen_test_subscribe_candlestick!(\n            BinanceInverseWSClient,\n            &[\n                (\"BTCUSD_221230\".to_string(), 60),\n                (\"ETHUSD_221230\".to_string(), 60),\n                (\"BNBUSD_221230\".to_string(), 60)\n            ]\n        );\n        gen_test_subscribe_candlestick!(\n            BinanceInverseWSClient,\n            &[\n                (\"BTCUSD_221230\".to_string(), 2592000),\n                (\"ETHUSD_221230\".to_string(), 2592000),\n                (\"BNBUSD_221230\".to_string(), 2592000)\n            ]\n        );\n    }\n}\n\n#[cfg(test)]\nmod binance_linear_future {\n    use crypto_ws_client::{BinanceLinearWSClient, WSClient};\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe() {\n        gen_test_code!(\n            BinanceLinearWSClient,\n            subscribe,\n            &[\n                (\"aggTrade\".to_string(), \"BTCUSDT_221230\".to_string()),\n                (\"aggTrade\".to_string(), \"ETHUSDT_221230\".to_string())\n            ]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_all_bbo() {\n        gen_test_code!(\n            BinanceLinearWSClient,\n            send,\n            &[r#\"{\"id\":9527,\"method\":\"SUBSCRIBE\",\"params\":[\"!bookTicker\"]}\"#.to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_trade() {\n        gen_test_code!(\n            BinanceLinearWSClient,\n            subscribe_trade,\n            &[\"BTCUSDT_221230\".to_string(), \"ETHUSDT_221230\".to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_ticker() {\n        gen_test_code!(\n            BinanceLinearWSClient,\n            subscribe_ticker,\n            &[\"BTCUSDT_221230\".to_string(), \"ETHUSDT_221230\".to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_tickers_all() {\n        gen_test_code!(\n            BinanceLinearWSClient,\n            send,\n            &[r#\"{\"id\":9527,\"method\":\"SUBSCRIBE\",\"params\":[\"!ticker@arr\"]}\"#.to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_bbo() {\n        gen_test_code!(\n            BinanceLinearWSClient,\n            subscribe_bbo,\n            &[\"BTCUSDT_221230\".to_string(), \"ETHUSDT_221230\".to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook() {\n        gen_test_code!(\n            BinanceLinearWSClient,\n            subscribe_orderbook,\n            &[\"BTCUSDT_221230\".to_string(), \"ETHUSDT_221230\".to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook_topk() {\n        gen_test_code!(\n            BinanceLinearWSClient,\n            subscribe_orderbook_topk,\n            &[\"BTCUSDT_221230\".to_string(), \"ETHUSDT_221230\".to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_candlestick() {\n        gen_test_subscribe_candlestick!(\n            BinanceLinearWSClient,\n            &[(\"BTCUSDT_221230\".to_string(), 60), (\"ETHUSDT_221230\".to_string(), 60)]\n        );\n        gen_test_subscribe_candlestick!(\n            BinanceLinearWSClient,\n            &[(\"BTCUSDT_221230\".to_string(), 2592000), (\"ETHUSDT_221230\".to_string(), 2592000)]\n        );\n    }\n}\n\n#[cfg(test)]\nmod binance_inverse_swap {\n    use crypto_ws_client::{BinanceInverseWSClient, WSClient};\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe() {\n        gen_test_code!(\n            BinanceInverseWSClient,\n            subscribe,\n            &[(\"aggTrade\".to_string(), \"btcusd_perp\".to_string())]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_all_bbo() {\n        gen_test_code!(\n            BinanceInverseWSClient,\n            send,\n            &[r#\"{\"id\":9527,\"method\":\"SUBSCRIBE\",\"params\":[\"!bookTicker\"]}\"#.to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_trade() {\n        gen_test_code!(\n            BinanceInverseWSClient,\n            subscribe_trade,\n            &[\"btcusd_perp\".to_string(), \"ethusd_perp\".to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_ticker() {\n        gen_test_code!(\n            BinanceInverseWSClient,\n            subscribe_ticker,\n            &[\"btcusd_perp\".to_string(), \"ethusd_perp\".to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_tickers_all() {\n        gen_test_code!(\n            BinanceInverseWSClient,\n            send,\n            &[r#\"{\"id\":9527,\"method\":\"SUBSCRIBE\",\"params\":[\"!ticker@arr\"]}\"#.to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_bbo() {\n        gen_test_code!(\n            BinanceInverseWSClient,\n            subscribe_bbo,\n            &[\"btcusd_perp\".to_string(), \"ethusd_perp\".to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook() {\n        gen_test_code!(\n            BinanceInverseWSClient,\n            subscribe_orderbook,\n            &[\"btcusd_perp\".to_string(), \"ethusd_perp\".to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook_topk() {\n        gen_test_code!(\n            BinanceInverseWSClient,\n            subscribe_orderbook_topk,\n            &[\"btcusd_perp\".to_string(), \"ethusd_perp\".to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_candlestick() {\n        gen_test_subscribe_candlestick!(\n            BinanceInverseWSClient,\n            &[(\"btcusd_perp\".to_string(), 60), (\"ethusd_perp\".to_string(), 60)]\n        );\n        gen_test_subscribe_candlestick!(\n            BinanceInverseWSClient,\n            &[(\"btcusd_perp\".to_string(), 2592000), (\"ethusd_perp\".to_string(), 2592000)]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_funding_rate() {\n        gen_test_code!(\n            BinanceInverseWSClient,\n            subscribe,\n            &[(\"markPrice\".to_string(), \"btcusd_perp\".to_string())]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_funding_rate_all() {\n        gen_test_code!(\n            BinanceInverseWSClient,\n            send,\n            &[r#\"{\"id\":9527,\"method\":\"SUBSCRIBE\",\"params\":[\"!markPrice@arr\"]}\"#.to_string()]\n        );\n    }\n}\n\n#[cfg(test)]\nmod binance_linear_swap {\n    use crypto_ws_client::{BinanceLinearWSClient, WSClient};\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe() {\n        gen_test_code!(\n            BinanceLinearWSClient,\n            subscribe,\n            &[(\"aggTrade\".to_string(), \"BTCUSDT\".to_string())]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_all_bbo() {\n        gen_test_code!(\n            BinanceLinearWSClient,\n            send,\n            &[r#\"{\"id\":9527,\"method\":\"SUBSCRIBE\",\"params\":[\"!bookTicker\"]}\"#.to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_trade() {\n        gen_test_code!(\n            BinanceLinearWSClient,\n            subscribe_trade,\n            &[\"BTCUSDT\".to_string(), \"ETHUSDT\".to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_ticker() {\n        gen_test_code!(\n            BinanceLinearWSClient,\n            subscribe_ticker,\n            &[\"BTCUSDT\".to_string(), \"ETHUSDT\".to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_tickers_all() {\n        gen_test_code!(\n            BinanceLinearWSClient,\n            send,\n            &[r#\"{\"id\":9527,\"method\":\"SUBSCRIBE\",\"params\":[\"!ticker@arr\"]}\"#.to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_bbo() {\n        gen_test_code!(\n            BinanceLinearWSClient,\n            subscribe_bbo,\n            &[\"BTCUSDT\".to_string(), \"ETHUSDT\".to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook() {\n        gen_test_code!(\n            BinanceLinearWSClient,\n            subscribe_orderbook,\n            &[\"BTCUSDT\".to_string(), \"ETHUSDT\".to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook_topk() {\n        gen_test_code!(\n            BinanceLinearWSClient,\n            subscribe_orderbook_topk,\n            &[\"BTCUSDT\".to_string(), \"ETHUSDT\".to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_candlestick() {\n        gen_test_subscribe_candlestick!(\n            BinanceLinearWSClient,\n            &[(\"BTCUSDT\".to_string(), 60), (\"ETHUSDT\".to_string(), 60)]\n        );\n        gen_test_subscribe_candlestick!(\n            BinanceLinearWSClient,\n            &[(\"BTCUSDT\".to_string(), 2592000), (\"ETHUSDT\".to_string(), 2592000)]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_funding_rate() {\n        gen_test_code!(\n            BinanceLinearWSClient,\n            subscribe,\n            &[(\"markPrice\".to_string(), \"BTCUSDT\".to_string())]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_funding_rate_all() {\n        gen_test_code!(\n            BinanceLinearWSClient,\n            send,\n            &[r#\"{\"id\":9527,\"method\":\"SUBSCRIBE\",\"params\":[\"!markPrice@arr\"]}\"#.to_string()]\n        );\n    }\n}\n"
  },
  {
    "path": "crypto-ws-client/tests/binance_option.rs",
    "content": "use crypto_ws_client::{BinanceOptionWSClient, WSClient};\n\n#[macro_use]\nmod utils;\n\n#[ignore]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn subscribe() {\n    gen_test_code!(\n        BinanceOptionWSClient,\n        subscribe,\n        &[\n            (\"TICKER_ALL\".to_string(), \"BTCUSDT\".to_string()),\n            (\"TRADE_ALL\".to_string(), \"BTCUSDT_C\".to_string()),\n            (\"TRADE_ALL\".to_string(), \"BTCUSDT_P\".to_string())\n        ]\n    );\n}\n\n#[tokio::test(flavor = \"multi_thread\")]\n#[ignore]\nasync fn subscribe_trade() {\n    gen_test_code!(\n        BinanceOptionWSClient,\n        subscribe_trade,\n        &[\"BTC-220325-40000-C\".to_string(), \"BTC-220325-35000-P\".to_string()]\n    );\n}\n\n#[tokio::test(flavor = \"multi_thread\")]\n#[ignore]\nasync fn subscribe_ticker() {\n    gen_test_code!(\n        BinanceOptionWSClient,\n        subscribe_ticker,\n        &[\"BTC-220325-40000-C\".to_string(), \"BTC-220325-35000-P\".to_string()]\n    );\n}\n\n#[ignore]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn subscribe_ticker_all() {\n    gen_test_code!(\n        BinanceOptionWSClient,\n        subscribe,\n        &[(\"TICKER_ALL\".to_string(), \"BTCUSDT\".to_string())]\n    );\n}\n\n#[tokio::test(flavor = \"multi_thread\")]\n#[ignore]\nasync fn subscribe_orderbook() {\n    gen_test_code!(\n        BinanceOptionWSClient,\n        subscribe_orderbook,\n        &[\"BTC-220325-40000-C\".to_string(), \"BTC-220325-35000-P\".to_string()]\n    );\n}\n\n#[tokio::test(flavor = \"multi_thread\")]\n#[ignore]\nasync fn subscribe_orderbook_topk() {\n    gen_test_code!(\n        BinanceOptionWSClient,\n        subscribe_orderbook_topk,\n        &[\"BTC-220325-40000-C\".to_string(), \"BTC-220325-35000-P\".to_string()]\n    );\n}\n\n#[tokio::test(flavor = \"multi_thread\")]\n#[ignore]\nasync fn subscribe_candlestick() {\n    gen_test_subscribe_candlestick!(\n        BinanceOptionWSClient,\n        &[(\"BTC-220325-40000-C\".to_string(), 60), (\"BTC-220325-35000-P\".to_string(), 60)]\n    );\n    gen_test_subscribe_candlestick!(\n        BinanceOptionWSClient,\n        &[(\"BTC-220325-40000-C\".to_string(), 60), (\"BTC-220325-35000-P\".to_string(), 60)]\n    );\n}\n"
  },
  {
    "path": "crypto-ws-client/tests/bitfinex.rs",
    "content": "#[macro_use]\nmod utils;\n\n#[cfg(test)]\nmod bitfinex_spot {\n    use crypto_ws_client::{BitfinexWSClient, WSClient};\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe() {\n        gen_test_code!(\n            BitfinexWSClient,\n            subscribe,\n            &[\n                (\"trades\".to_string(), \"tBTCUST\".to_string()),\n                (\"trades\".to_string(), \"tETHUST\".to_string())\n            ]\n        );\n    }\n\n    #[test]\n    #[should_panic]\n    fn subscribe_illegal_symbol() {\n        gen_test_code!(\n            BitfinexWSClient,\n            subscribe,\n            &[(\"trades\".to_string(), \"tXXXYYY\".to_string())]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_trade() {\n        gen_test_code!(\n            BitfinexWSClient,\n            subscribe_trade,\n            &[\"tBTCUST\".to_string(), \"tETHUST\".to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_ticker() {\n        gen_test_code!(BitfinexWSClient, subscribe_ticker, &[\"tBTCUST\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook() {\n        gen_test_code!(BitfinexWSClient, subscribe_orderbook, &[\"tBTCUST\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_l3_orderbook() {\n        gen_test_code!(BitfinexWSClient, subscribe_l3_orderbook, &[\"tBTCUST\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_candlestick() {\n        gen_test_subscribe_candlestick!(BitfinexWSClient, &[(\"tBTCUST\".to_string(), 60)]);\n        gen_test_subscribe_candlestick!(BitfinexWSClient, &[(\"tBTCUST\".to_string(), 2592000)]);\n    }\n}\n\n#[cfg(test)]\nmod bitfinex_swap {\n    use crypto_ws_client::{BitfinexWSClient, WSClient};\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe() {\n        gen_test_code!(\n            BitfinexWSClient,\n            subscribe,\n            &[(\"trades\".to_string(), \"tBTCF0:USTF0\".to_string())]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_trade() {\n        gen_test_code!(\n            BitfinexWSClient,\n            subscribe_trade,\n            &[\"tBTCF0:USTF0\".to_string(), \"tETHF0:USTF0\".to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_ticker() {\n        gen_test_code!(BitfinexWSClient, subscribe_ticker, &[\"tBTCF0:USTF0\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook() {\n        gen_test_code!(BitfinexWSClient, subscribe_orderbook, &[\"tBTCF0:USTF0\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_l3_orderbook() {\n        gen_test_code!(\n            BitfinexWSClient,\n            subscribe_l3_orderbook,\n            &[\"tBTCF0:USTF0\".to_string(), \"tETHF0:USTF0\".to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_candlestick() {\n        gen_test_subscribe_candlestick!(BitfinexWSClient, &[(\"tBTCF0:USTF0\".to_string(), 60)]);\n        gen_test_subscribe_candlestick!(BitfinexWSClient, &[(\"tBTCF0:USTF0\".to_string(), 2592000)]);\n    }\n}\n"
  },
  {
    "path": "crypto-ws-client/tests/bitget.rs",
    "content": "#[macro_use]\nmod utils;\n\n#[cfg(test)]\nmod bitget_spot {\n    use crypto_ws_client::{BitgetSpotWSClient, WSClient};\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_trade() {\n        gen_test_code!(BitgetSpotWSClient, subscribe_trade, &[\"BTCUSDT\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook_topk() {\n        gen_test_code!(BitgetSpotWSClient, subscribe_orderbook_topk, &[\"BTCUSDT\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook() {\n        gen_test_code!(BitgetSpotWSClient, subscribe_orderbook, &[\"BTCUSDT\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_ticker() {\n        gen_test_code!(BitgetSpotWSClient, subscribe_ticker, &[\"BTCUSDT\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_candlestick() {\n        gen_test_subscribe_candlestick!(BitgetSpotWSClient, &[(\"BTCUSDT\".to_string(), 60)]);\n        gen_test_subscribe_candlestick!(BitgetSpotWSClient, &[(\"BTCUSDT\".to_string(), 604800)]);\n    }\n}\n\n#[cfg(test)]\nmod bitget_inverse_swap {\n    use crypto_ws_client::{BitgetSwapWSClient, WSClient};\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe() {\n        gen_test_code!(\n            BitgetSwapWSClient,\n            subscribe,\n            &[(\"trade\".to_string(), \"BTCUSD\".to_string())]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_raw_json() {\n        gen_test_code!(\n            BitgetSwapWSClient,\n            send,\n            &[r#\"{\"op\":\"subscribe\",\"args\":[{\"channel\":\"trade\",\"instId\":\"BTCUSD\",\"instType\":\"MC\"}]}\"#.to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_trade() {\n        gen_test_code!(BitgetSwapWSClient, subscribe_trade, &[\"BTCUSD\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook_topk() {\n        gen_test_code!(BitgetSwapWSClient, subscribe_orderbook_topk, &[\"BTCUSD\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook() {\n        gen_test_code!(BitgetSwapWSClient, subscribe_orderbook, &[\"BTCUSD\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_ticker() {\n        gen_test_code!(BitgetSwapWSClient, subscribe_ticker, &[\"BTCUSD\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_candlestick() {\n        gen_test_subscribe_candlestick!(BitgetSwapWSClient, &[(\"BTCUSD\".to_string(), 60)]);\n        gen_test_subscribe_candlestick!(BitgetSwapWSClient, &[(\"BTCUSD\".to_string(), 604800)]);\n    }\n\n    #[ignore]\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_funding_rate() {\n        gen_test_code!(\n            BitgetSwapWSClient,\n            subscribe,\n            &[(\"funding_rate\".to_string(), \"BTCUSD\".to_string())]\n        );\n    }\n}\n\n#[cfg(test)]\nmod bitget_linear_swap {\n    use crypto_ws_client::{BitgetSwapWSClient, WSClient};\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_trade() {\n        gen_test_code!(BitgetSwapWSClient, subscribe_trade, &[\"BTCUSDT\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook_topk() {\n        gen_test_code!(BitgetSwapWSClient, subscribe_orderbook_topk, &[\"BTCUSDT\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook() {\n        gen_test_code!(BitgetSwapWSClient, subscribe_orderbook, &[\"BTCUSDT\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_ticker() {\n        gen_test_code!(BitgetSwapWSClient, subscribe_ticker, &[\"BTCUSDT\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_candlestick() {\n        gen_test_subscribe_candlestick!(BitgetSwapWSClient, &[(\"BTCUSDT\".to_string(), 60)]);\n        gen_test_subscribe_candlestick!(BitgetSwapWSClient, &[(\"BTCUSDT\".to_string(), 604800)]);\n    }\n\n    #[ignore]\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_funding_rate() {\n        gen_test_code!(\n            BitgetSwapWSClient,\n            subscribe,\n            &[(\"funding_rate\".to_string(), \"BTCUSDT\".to_string())]\n        );\n    }\n}\n"
  },
  {
    "path": "crypto-ws-client/tests/bithumb.rs",
    "content": "use crypto_ws_client::{BithumbWSClient, WSClient};\n\n#[macro_use]\nmod utils;\n\n#[ignore = \"duplicated\"]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn subscribe() {\n    gen_test_code!(\n        BithumbWSClient,\n        subscribe,\n        &[\n            (\"TRADE\".to_string(), \"BTC-USDT\".to_string()),\n            (\"TRADE\".to_string(), \"ETH-USDT\".to_string())\n        ]\n    );\n}\n\n#[test]\n#[should_panic]\nfn subscribe_illegal_symbol() {\n    gen_test_code!(BithumbWSClient, subscribe, &[(\"TRADE\".to_string(), \"XXX-YYY\".to_string())]);\n}\n\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn subscribe_raw_json() {\n    gen_test_code!(\n        BithumbWSClient,\n        send,\n        &[r#\"{\"cmd\":\"subscribe\",\"args\":[\"TRADE:BTC-USDT\",\"TRADE:ETH-USDT\"]}\"#.to_string()]\n    );\n}\n\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn subscribe_trade() {\n    gen_test_code!(\n        BithumbWSClient,\n        subscribe_trade,\n        &[\"BTC-USDT\".to_string(), \"ETH-USDT\".to_string()]\n    );\n}\n\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn subscribe_orderbook() {\n    gen_test_code!(\n        BithumbWSClient,\n        subscribe_orderbook,\n        &[\"BTC-USDT\".to_string(), \"ETH-USDT\".to_string()]\n    );\n}\n\n#[ignore = \"too slow\"]\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn subscribe_ticker() {\n    gen_test_code!(\n        BithumbWSClient,\n        subscribe_ticker,\n        &[\"BTC-USDT\".to_string(), \"ETH-USDT\".to_string()]\n    );\n}\n"
  },
  {
    "path": "crypto-ws-client/tests/bitmex.rs",
    "content": "use crypto_ws_client::{BitmexWSClient, WSClient};\n\n#[macro_use]\nmod utils;\n\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn bitmex_instrument() {\n    gen_test_code!(\n        BitmexWSClient,\n        send,\n        &[r#\"{\"op\":\"subscribe\",\"args\":[\"instrument\"]}\"#.to_string()]\n    );\n}\n\n#[cfg(test)]\nmod bitmex_inverse_swap {\n    use crypto_ws_client::{BitmexWSClient, WSClient};\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe() {\n        gen_test_code!(\n            BitmexWSClient,\n            subscribe,\n            &[\n                (\"trade\".to_string(), \"XBTUSD\".to_string()),\n                (\"quote\".to_string(), \"XBTUSD\".to_string())\n            ]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_raw_json() {\n        gen_test_code!(\n            BitmexWSClient,\n            send,\n            &[r#\"{\"op\":\"subscribe\",\"args\":[\"trade:XBTUSD\",\"quote:XBTUSD\"]}\"#.to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_trade() {\n        gen_test_code!(BitmexWSClient, subscribe_trade, &[\"XBTUSD\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_bbo() {\n        gen_test_code!(BitmexWSClient, subscribe_bbo, &[\"XBTUSD\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook() {\n        gen_test_code!(BitmexWSClient, subscribe_orderbook, &[\"XBTUSD\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook_topk() {\n        gen_test_code!(BitmexWSClient, subscribe_orderbook_topk, &[\"XBTUSD\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_candlestick() {\n        gen_test_subscribe_candlestick!(BitmexWSClient, &[(\"XBTUSD\".to_string(), 60)]);\n        gen_test_subscribe_candlestick!(BitmexWSClient, &[(\"XBTUSD\".to_string(), 86400)]);\n    }\n\n    #[test]\n    #[ignore]\n    fn subscribe_funding_rate() {\n        gen_test_code!(BitmexWSClient, subscribe, &[(\"funding\".to_string(), \"XBTUSD\".to_string())]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_funding_rate_all() {\n        gen_test_code!(\n            BitmexWSClient,\n            send,\n            &[r#\"{\"op\":\"subscribe\",\"args\":[\"funding\"]}\"#.to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_instrument() {\n        gen_test_code!(\n            BitmexWSClient,\n            subscribe,\n            &[(\"instrument\".to_string(), \"XBTUSD\".to_string())]\n        );\n    }\n}\n\n#[cfg(test)]\nmod bitmex_inverse_future {\n    use crypto_ws_client::{BitmexWSClient, WSClient};\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe() {\n        gen_test_code!(\n            BitmexWSClient,\n            subscribe,\n            &[\n                (\"trade\".to_string(), \"XBTZ22\".to_string()),\n                (\"quote\".to_string(), \"XBTZ22\".to_string())\n            ]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_trade() {\n        gen_test_code!(\n            BitmexWSClient,\n            subscribe_trade,\n            &[\"XBTZ22\".to_string(), \"XBTZ22\".to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_bbo() {\n        gen_test_code!(\n            BitmexWSClient,\n            subscribe_bbo,\n            &[\"XBTZ22\".to_string(), \"XBTZ22\".to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook() {\n        gen_test_code!(\n            BitmexWSClient,\n            subscribe_orderbook,\n            &[\"XBTZ22\".to_string(), \"XBTZ22\".to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook_topk() {\n        gen_test_code!(\n            BitmexWSClient,\n            subscribe_orderbook_topk,\n            &[\"XBTZ22\".to_string(), \"XBTZ22\".to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_candlestick() {\n        gen_test_subscribe_candlestick!(\n            BitmexWSClient,\n            &[(\"XBTZ22\".to_string(), 60), (\"XBTZ22\".to_string(), 60)]\n        );\n        gen_test_subscribe_candlestick!(\n            BitmexWSClient,\n            &[(\"XBTZ22\".to_string(), 86400), (\"XBTZ22\".to_string(), 86400)]\n        );\n    }\n}\n\n#[cfg(test)]\nmod bitmex_quanto_swap {\n    use crypto_ws_client::{BitmexWSClient, WSClient};\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_trade() {\n        gen_test_code!(BitmexWSClient, subscribe_trade, &[\"ETHUSD\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_bbo() {\n        gen_test_code!(BitmexWSClient, subscribe_bbo, &[\"ETHUSD\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook() {\n        gen_test_code!(BitmexWSClient, subscribe_orderbook, &[\"ETHUSD\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook_topk() {\n        gen_test_code!(BitmexWSClient, subscribe_orderbook_topk, &[\"ETHUSD\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_candlestick() {\n        gen_test_subscribe_candlestick!(BitmexWSClient, &[(\"ETHUSD\".to_string(), 60)]);\n        gen_test_subscribe_candlestick!(BitmexWSClient, &[(\"ETHUSD\".to_string(), 86400)]);\n    }\n\n    #[test]\n    #[ignore]\n    fn subscribe_funding_rate() {\n        gen_test_code!(BitmexWSClient, subscribe, &[(\"funding\".to_string(), \"ETHUSD\".to_string())]);\n    }\n}\n\n#[cfg(test)]\nmod bitmex_linear_future {\n    use crypto_ws_client::{BitmexWSClient, WSClient};\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_trade() {\n        gen_test_code!(\n            BitmexWSClient,\n            subscribe_trade,\n            &[\"XBTUSDTZ22\".to_string(), \"ETHZ22\".to_string(), \"ETHUSDTZ22\".to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_bbo() {\n        gen_test_code!(BitmexWSClient, subscribe_bbo, &[\"ETHZ22\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook() {\n        gen_test_code!(BitmexWSClient, subscribe_orderbook, &[\"ETHZ22\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook_topk() {\n        gen_test_code!(BitmexWSClient, subscribe_orderbook_topk, &[\"ETHZ22\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_candlestick() {\n        gen_test_subscribe_candlestick!(BitmexWSClient, &[(\"ETHZ22\".to_string(), 60)]);\n        gen_test_subscribe_candlestick!(BitmexWSClient, &[(\"ETHZ22\".to_string(), 86400)]);\n    }\n}\n"
  },
  {
    "path": "crypto-ws-client/tests/bitstamp.rs",
    "content": "use crypto_ws_client::{BitstampWSClient, WSClient};\n\n#[macro_use]\nmod utils;\n\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn subscribe() {\n    gen_test_code!(\n        BitstampWSClient,\n        subscribe,\n        &[\n            (\"live_trades\".to_string(), \"btcusd\".to_string()),\n            (\"diff_order_book\".to_string(), \"btcusd\".to_string())\n        ]\n    );\n}\n\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn subscribe_raw_json() {\n    gen_test_code!(\n        BitstampWSClient,\n        send,\n        &[\n            r#\"{\"event\":\"bts:subscribe\",\"data\":{\"channel\":\"live_trades_btcusd\"}}\"#.to_string(),\n            r#\"{\"event\":\"bts:subscribe\",\"data\":{\"channel\":\"live_trades_ethusd\"}}\"#.to_string()\n        ]\n    );\n}\n\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn subscribe_trade() {\n    gen_test_code!(\n        BitstampWSClient,\n        subscribe_trade,\n        &[\"btcusd\".to_string(), \"ethusd\".to_string()]\n    );\n}\n\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn subscribe_orderbook() {\n    gen_test_code!(\n        BitstampWSClient,\n        subscribe_orderbook,\n        &[\"btcusd\".to_string(), \"ethusd\".to_string()]\n    );\n}\n\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn subscribe_orderbook_topk() {\n    gen_test_code!(\n        BitstampWSClient,\n        subscribe_orderbook_topk,\n        &[\"btcusd\".to_string(), \"ethusd\".to_string()]\n    );\n}\n\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn subscribe_l3_orderbook() {\n    gen_test_code!(\n        BitstampWSClient,\n        subscribe_l3_orderbook,\n        &[\"btcusd\".to_string(), \"ethusd\".to_string()]\n    );\n}\n"
  },
  {
    "path": "crypto-ws-client/tests/bitz.rs",
    "content": "#[macro_use]\nmod utils;\n\n#[cfg(test)]\nmod bitz_spot {\n    use crypto_ws_client::{BitzSpotWSClient, WSClient};\n    use std::time::{SystemTime, UNIX_EPOCH};\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    #[ignore = \"bitz.com has shutdown since October 2021\"]\n    async fn subscribe() {\n        gen_test_code!(\n            BitzSpotWSClient,\n            subscribe,\n            &[\n                (\"market\".to_string(), \"btc_usdt\".to_string()),\n                (\"depth\".to_string(), \"btc_usdt\".to_string()),\n                (\"order\".to_string(), \"btc_usdt\".to_string())\n            ]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    #[ignore = \"bitz.com has shutdown since October 2021\"]\n    async fn subscribe_raw_json() {\n        gen_test_code!(\n            BitzSpotWSClient,\n            send,\n            &[format!(\n                r#\"{{\"action\":\"Topic.sub\", \"data\":{{\"symbol\":\"btc_usdt\", \"type\":\"market,depth,order\", \"_CDID\":\"100002\", \"dataType\":\"1\"}}, \"msg_id\":{}}}\"#,\n                SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis()\n            )]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    #[ignore = \"bitz.com has shutdown since October 2021\"]\n    async fn subscribe_trade() {\n        gen_test_code!(BitzSpotWSClient, subscribe_trade, &[\"btc_usdt\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    #[ignore = \"bitz.com has shutdown since October 2021\"]\n    async fn subscribe_orderbook() {\n        gen_test_code!(BitzSpotWSClient, subscribe_orderbook, &[\"btc_usdt\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    #[ignore = \"bitz.com has shutdown since October 2021\"]\n    async fn subscribe_ticker() {\n        gen_test_code!(BitzSpotWSClient, subscribe_ticker, &[\"btc_usdt\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    #[ignore = \"bitz.com has shutdown since October 2021\"]\n    async fn subscribe_candlestick() {\n        gen_test_subscribe_candlestick!(BitzSpotWSClient, &[(\"btc_usdt\".to_string(), 60)]);\n        gen_test_subscribe_candlestick!(BitzSpotWSClient, &[(\"btc_usdt\".to_string(), 2592000)]);\n    }\n}\n"
  },
  {
    "path": "crypto-ws-client/tests/bybit.rs",
    "content": "#[macro_use]\nmod utils;\n\n#[cfg(test)]\nmod bybit_inverse_future {\n    use crypto_ws_client::{BybitInverseWSClient, WSClient};\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe() {\n        gen_test_code!(\n            BybitInverseWSClient,\n            subscribe,\n            &[(\"trade\".to_string(), \"BTCUSDZ22\".to_string())]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_raw_json() {\n        gen_test_code!(\n            BybitInverseWSClient,\n            send,\n            &[r#\"{\"op\":\"subscribe\",\"args\":[\"trade.BTCUSDZ22\"]}\"#.to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_trade() {\n        gen_test_code!(BybitInverseWSClient, subscribe_trade, &[\"BTCUSDZ22\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook() {\n        gen_test_code!(BybitInverseWSClient, subscribe_orderbook, &[\"BTCUSDZ22\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_ticker() {\n        gen_test_code!(BybitInverseWSClient, subscribe_ticker, &[\"BTCUSDZ22\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_candlestick() {\n        gen_test_subscribe_candlestick!(BybitInverseWSClient, &[(\"BTCUSDZ22\".to_string(), 60)]);\n        gen_test_subscribe_candlestick!(\n            BybitInverseWSClient,\n            &[(\"BTCUSDZ22\".to_string(), 2592000)]\n        );\n    }\n}\n\n#[cfg(test)]\nmod bybit_inverse_swap {\n    use crypto_ws_client::{BybitInverseWSClient, WSClient};\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe() {\n        gen_test_code!(\n            BybitInverseWSClient,\n            subscribe,\n            &[(\"trade\".to_string(), \"BTCUSD\".to_string())]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_raw_json() {\n        gen_test_code!(\n            BybitInverseWSClient,\n            send,\n            &[r#\"{\"op\":\"subscribe\",\"args\":[\"trade.BTCUSD\"]}\"#.to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_trade() {\n        gen_test_code!(BybitInverseWSClient, subscribe_trade, &[\"BTCUSD\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook() {\n        gen_test_code!(BybitInverseWSClient, subscribe_orderbook, &[\"BTCUSD\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_ticker() {\n        gen_test_code!(BybitInverseWSClient, subscribe_ticker, &[\"BTCUSD\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_candlestick() {\n        gen_test_subscribe_candlestick!(BybitInverseWSClient, &[(\"BTCUSD\".to_string(), 60)]);\n        gen_test_subscribe_candlestick!(BybitInverseWSClient, &[(\"BTCUSD\".to_string(), 2592000)]);\n    }\n}\n\n#[cfg(test)]\nmod bybit_linear_swap {\n    use crypto_ws_client::{BybitLinearSwapWSClient, WSClient};\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_trade() {\n        gen_test_code!(BybitLinearSwapWSClient, subscribe_trade, &[\"BTCUSDT\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook() {\n        gen_test_code!(BybitLinearSwapWSClient, subscribe_orderbook, &[\"BTCUSDT\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_ticker() {\n        gen_test_code!(BybitLinearSwapWSClient, subscribe_ticker, &[\"BTCUSDT\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_candlestick() {\n        gen_test_subscribe_candlestick!(BybitLinearSwapWSClient, &[(\"BTCUSDT\".to_string(), 60)]);\n        gen_test_subscribe_candlestick!(\n            BybitLinearSwapWSClient,\n            &[(\"BTCUSDT\".to_string(), 2592000)]\n        );\n    }\n}\n"
  },
  {
    "path": "crypto-ws-client/tests/coinbase_pro.rs",
    "content": "use crypto_ws_client::{CoinbaseProWSClient, WSClient};\n\n#[macro_use]\nmod utils;\n\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn subscribe() {\n    gen_test_code!(\n        CoinbaseProWSClient,\n        subscribe,\n        &[\n            (\"matches\".to_string(), \"BTC-USD\".to_string()),\n            (\"heartbeat\".to_string(), \"BTC-USD\".to_string())\n        ]\n    );\n}\n\n#[tokio::test(flavor = \"multi_thread\")]\n#[should_panic]\nasync fn subscribe_illegal_symbol() {\n    gen_test_code!(\n        CoinbaseProWSClient,\n        subscribe,\n        &[(\"matches\".to_string(), \"XXX-YYY\".to_string())]\n    );\n}\n\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn subscribe_raw_json() {\n    gen_test_code!(\n        CoinbaseProWSClient,\n        send,\n        &[r#\"{\n                \"type\":\"subscribe\",\n                \"channels\":[\n                   {\n                      \"name\":\"heartbeat\",\n                      \"product_ids\":[\n                         \"BTC-USD\"\n                      ]\n                   },\n                   {\n                      \"name\":\"matches\",\n                      \"product_ids\":[\n                         \"BTC-USD\"\n                      ]\n                   }\n                ]\n             }\"#\n        .to_string()]\n    );\n}\n\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn subscribe_trade() {\n    gen_test_code!(\n        CoinbaseProWSClient,\n        subscribe_trade,\n        &[\"BTC-USD\".to_string(), \"ETH-USD\".to_string()]\n    );\n}\n\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn subscribe_ticker() {\n    gen_test_code!(\n        CoinbaseProWSClient,\n        subscribe_ticker,\n        &[\"BTC-USD\".to_string(), \"ETH-USD\".to_string()]\n    );\n}\n\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn subscribe_orderbook() {\n    gen_test_code!(CoinbaseProWSClient, subscribe_orderbook, &[\"BTC-USD\".to_string()]);\n}\n\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn subscribe_l3_orderbook() {\n    gen_test_code!(CoinbaseProWSClient, subscribe_l3_orderbook, &[\"BTC-USD\".to_string()]);\n}\n"
  },
  {
    "path": "crypto-ws-client/tests/deribit.rs",
    "content": "use crypto_ws_client::{DeribitWSClient, WSClient};\n\n#[macro_use]\nmod utils;\n\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn deribit_all_trades() {\n    gen_test_code!(\n        DeribitWSClient,\n        subscribe,\n        // https://docs.deribit.com/?javascript#trades-kind-currency-interval\n        &[\n            (\"trades.future.SYMBOL.100ms\".to_string(), \"any\".to_string()),\n            (\"trades.option.SYMBOL.100ms\".to_string(), \"any\".to_string())\n        ]\n    );\n}\n\n#[cfg(test)]\nmod deribit_inverse_future {\n    use crypto_ws_client::{DeribitWSClient, WSClient};\n\n    #[ignore = \"lack of liquidity\"]\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe() {\n        gen_test_code!(\n            DeribitWSClient,\n            subscribe,\n            &[(\"trades.future.SYMBOL.100ms\".to_string(), \"BTC\".to_string())]\n        );\n    }\n\n    #[ignore = \"lack of liquidity\"]\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_trade() {\n        gen_test_code!(\n            DeribitWSClient,\n            subscribe_trade,\n            &[\"BTC-26AUG22\".to_string(), \"BTC-30DEC22\".to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_ticker() {\n        gen_test_code!(DeribitWSClient, subscribe_ticker, &[\"BTC-30DEC22\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook() {\n        gen_test_code!(DeribitWSClient, subscribe_orderbook, &[\"BTC-30DEC22\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook_topk() {\n        gen_test_code!(DeribitWSClient, subscribe_orderbook_topk, &[\"BTC-30DEC22\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_bbo() {\n        gen_test_code!(DeribitWSClient, subscribe_bbo, &[\"BTC-30DEC22\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_candlestick() {\n        gen_test_subscribe_candlestick!(DeribitWSClient, &[(\"BTC-30DEC22\".to_string(), 60)]);\n        gen_test_subscribe_candlestick!(DeribitWSClient, &[(\"BTC-30DEC22\".to_string(), 86400)]);\n    }\n}\n\n#[cfg(test)]\nmod deribit_inverse_swap {\n    use crypto_ws_client::{DeribitWSClient, WSClient};\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe() {\n        gen_test_code!(\n            DeribitWSClient,\n            subscribe,\n            &[(\"trades.SYMBOL.100ms\".to_string(), \"BTC-PERPETUAL\".to_string())]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_trade() {\n        gen_test_code!(DeribitWSClient, subscribe_trade, &[\"BTC-PERPETUAL\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_ticker() {\n        gen_test_code!(DeribitWSClient, subscribe_ticker, &[\"BTC-PERPETUAL\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook() {\n        gen_test_code!(DeribitWSClient, subscribe_orderbook, &[\"BTC-PERPETUAL\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook_topk() {\n        gen_test_code!(DeribitWSClient, subscribe_orderbook_topk, &[\"BTC-PERPETUAL\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_bbo() {\n        gen_test_code!(DeribitWSClient, subscribe_bbo, &[\"BTC-PERPETUAL\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_candlestick() {\n        gen_test_subscribe_candlestick!(DeribitWSClient, &[(\"BTC-PERPETUAL\".to_string(), 60)]);\n        gen_test_subscribe_candlestick!(DeribitWSClient, &[(\"BTC-PERPETUAL\".to_string(), 86400)]);\n    }\n}\n\n#[cfg(test)]\nmod deribit_option {\n    use crypto_ws_client::{DeribitWSClient, WSClient};\n\n    const SYMBOLS: &[&str] = &[\n        \"BTC-26AUG22-23000-C\",\n        \"BTC-26AUG22-45000-C\",\n        \"BTC-30DEC22-40000-C\",\n        \"BTC-30DEC22-60000-C\",\n    ];\n\n    #[ignore = \"lack of liquidity\"]\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe() {\n        gen_test_code!(\n            DeribitWSClient,\n            subscribe,\n            &[(\"trades.option.SYMBOL.100ms\".to_string(), \"any\".to_string())]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    #[ignore]\n    async fn subscribe_trade() {\n        gen_test_code!(\n            DeribitWSClient,\n            subscribe_trade,\n            &SYMBOLS.iter().map(|s| s.to_string()).collect::<Vec<String>>()\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_ticker() {\n        gen_test_code!(\n            DeribitWSClient,\n            subscribe_ticker,\n            &SYMBOLS.iter().map(|s| s.to_string()).collect::<Vec<String>>()\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    #[ignore]\n    async fn subscribe_orderbook() {\n        gen_test_code!(\n            DeribitWSClient,\n            subscribe_orderbook,\n            &SYMBOLS.iter().map(|s| s.to_string()).collect::<Vec<String>>()\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    #[ignore]\n    async fn subscribe_orderbook_topk() {\n        gen_test_code!(\n            DeribitWSClient,\n            subscribe_orderbook_topk,\n            &SYMBOLS.iter().map(|s| s.to_string()).collect::<Vec<String>>()\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_bbo() {\n        gen_test_code!(\n            DeribitWSClient,\n            subscribe_bbo,\n            &SYMBOLS.iter().map(|s| s.to_string()).collect::<Vec<String>>()\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_candlestick() {\n        gen_test_subscribe_candlestick!(\n            DeribitWSClient,\n            SYMBOLS\n                .iter()\n                .map(|s| (s.to_string(), 60))\n                .collect::<Vec<(String, usize)>>()\n                .as_slice()\n        );\n        gen_test_subscribe_candlestick!(\n            DeribitWSClient,\n            SYMBOLS\n                .iter()\n                .map(|s| (s.to_string(), 86400))\n                .collect::<Vec<(String, usize)>>()\n                .as_slice()\n        );\n    }\n}\n"
  },
  {
    "path": "crypto-ws-client/tests/dydx.rs",
    "content": "#[macro_use]\nmod utils;\n\n#[cfg(test)]\nmod dydx_linear_swap {\n    use crypto_ws_client::{DydxSwapWSClient, WSClient};\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe() {\n        gen_test_code!(\n            DydxSwapWSClient,\n            send,\n            &[\n                r#\"{\"type\": \"subscribe\", \"channel\": \"v3_trades\", \"id\": \"BTC-USD\"}\"#.to_string(),\n                r#\"{\"type\": \"subscribe\", \"channel\": \"v3_trades\", \"id\": \"ETH-USD\"}\"#.to_string()\n            ]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_trade() {\n        gen_test_code!(\n            DydxSwapWSClient,\n            subscribe_trade,\n            &[\"BTC-USD\".to_string(), \"ETH-USD\".to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook() {\n        gen_test_code!(\n            DydxSwapWSClient,\n            subscribe_orderbook,\n            &[\"BTC-USD\".to_string(), \"ETH-USD\".to_string()]\n        );\n    }\n}\n"
  },
  {
    "path": "crypto-ws-client/tests/ftx.rs",
    "content": "#[macro_use]\nmod utils;\n\n#[cfg(test)]\nmod ftx_spot {\n    use crypto_ws_client::{FtxWSClient, WSClient};\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe() {\n        gen_test_code!(FtxWSClient, subscribe, &[(\"trades\".to_string(), \"BTC/USD\".to_string())]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_raw_json() {\n        gen_test_code!(\n            FtxWSClient,\n            send,\n            &[r#\"{\"op\":\"subscribe\",\"channel\":\"trades\",\"market\":\"BTC/USD\"}\"#.to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_trade() {\n        gen_test_code!(FtxWSClient, subscribe_trade, &[\"BTC/USD\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_bbo() {\n        gen_test_code!(FtxWSClient, subscribe_bbo, &[\"BTC/USD\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook() {\n        gen_test_code!(FtxWSClient, subscribe_orderbook, &[\"BTC/USD\".to_string()]);\n    }\n}\n\n#[cfg(test)]\nmod ftx_linear_swap {\n    use crypto_ws_client::{FtxWSClient, WSClient};\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_trade() {\n        gen_test_code!(FtxWSClient, subscribe_trade, &[\"BTC-PERP\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_bbo() {\n        gen_test_code!(FtxWSClient, subscribe_bbo, &[\"BTC-PERP\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook() {\n        gen_test_code!(\n            FtxWSClient,\n            subscribe_orderbook,\n            &[\"BTC-PERP\".to_string(), \"ETH-PERP\".to_string()]\n        );\n    }\n}\n\n#[cfg(test)]\nmod ftx_linear_future {\n    use crypto_ws_client::{FtxWSClient, WSClient};\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_trade() {\n        gen_test_code!(\n            FtxWSClient,\n            subscribe_trade,\n            &[\"BTC-1230\".to_string(), \"ETH-1230\".to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_bbo() {\n        gen_test_code!(\n            FtxWSClient,\n            subscribe_bbo,\n            &[\"BTC-1230\".to_string(), \"ETH-1230\".to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook() {\n        gen_test_code!(FtxWSClient, subscribe_orderbook, &[\"BTC-1230\".to_string()]);\n    }\n}\n\n#[cfg(test)]\nmod ftx_move {\n    use crypto_ws_client::{FtxWSClient, WSClient};\n\n    #[test]\n    #[ignore]\n    fn subscribe_trade() {\n        gen_test_code!(FtxWSClient, subscribe_trade, &[\"BTC-MOVE-2022Q4\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_bbo() {\n        gen_test_code!(FtxWSClient, subscribe_bbo, &[\"BTC-MOVE-2022Q4\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook() {\n        gen_test_code!(FtxWSClient, subscribe_orderbook, &[\"BTC-MOVE-2022Q4\".to_string()]);\n    }\n}\n\n#[cfg(test)]\nmod ftx_bvol {\n    use crypto_ws_client::{FtxWSClient, WSClient};\n\n    #[test]\n    #[ignore]\n    fn subscribe_trade() {\n        gen_test_code!(FtxWSClient, subscribe_trade, &[\"BVOL/USD\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_bbo() {\n        gen_test_code!(FtxWSClient, subscribe_bbo, &[\"BVOL/USD\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook() {\n        gen_test_code!(FtxWSClient, subscribe_orderbook, &[\"BVOL/USD\".to_string()]);\n    }\n}\n"
  },
  {
    "path": "crypto-ws-client/tests/gate.rs",
    "content": "#[macro_use]\nmod utils;\n\n#[cfg(test)]\nmod gate_spot {\n    use crypto_ws_client::{GateSpotWSClient, WSClient};\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe() {\n        gen_test_code!(\n            GateSpotWSClient,\n            subscribe,\n            &[\n                (\"trades\".to_string(), \"BTC_USDT\".to_string()),\n                (\"trades\".to_string(), \"ETH_USDT\".to_string())\n            ]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_raw_json() {\n        gen_test_code!(\n            GateSpotWSClient,\n            send,\n            &[r#\"{\"channel\":\"spot.trades\", \"event\":\"subscribe\", \"payload\":[\"BTC_USDT\",\"ETH_USDT\"]}\"#.to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_trade() {\n        gen_test_code!(GateSpotWSClient, subscribe_trade, &[\"BTC_USDT\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook() {\n        gen_test_code!(GateSpotWSClient, subscribe_orderbook, &[\"BTC_USDT\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook_topk() {\n        gen_test_code!(GateSpotWSClient, subscribe_orderbook_topk, &[\"BTC_USDT\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_bbo() {\n        gen_test_code!(GateSpotWSClient, subscribe_bbo, &[\"BTC_USDT\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_ticker() {\n        gen_test_code!(GateSpotWSClient, subscribe_ticker, &[\"BTC_USDT\".to_string()]);\n    }\n\n    #[ignore = \"too slow\"]\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_candlestick() {\n        gen_test_subscribe_candlestick!(GateSpotWSClient, &[(\"BTC_USDT\".to_string(), 10)]);\n        gen_test_subscribe_candlestick!(GateSpotWSClient, &[(\"BTC_USDT\".to_string(), 604800)]);\n    }\n}\n\n#[cfg(test)]\nmod gate_inverse_swap {\n    use crypto_ws_client::{GateInverseSwapWSClient, WSClient};\n\n    #[ignore = \"lack of liquidity\"]\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe() {\n        gen_test_code!(\n            GateInverseSwapWSClient,\n            subscribe,\n            &[\n                (\"trades\".to_string(), \"BTC_USD\".to_string()),\n                (\"trades\".to_string(), \"ETH_USD\".to_string()),\n                (\"trades\".to_string(), \"BNB_USD\".to_string()),\n                (\"trades\".to_string(), \"XRP_USD\".to_string())\n            ]\n        );\n    }\n\n    #[ignore = \"lack of liquidity\"]\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_raw_json() {\n        gen_test_code!(\n            GateInverseSwapWSClient,\n            send,\n            &[r#\"{\"channel\":\"futures.trades\", \"event\":\"subscribe\", \"payload\":[\"BTC_USD\",\"ETH_USD\",\"BNB_USD\",\"XRP_USD\"]}\"#\n                    .to_string()]\n        );\n    }\n\n    #[ignore = \"lack of liquidity\"]\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_trade() {\n        gen_test_code!(\n            GateInverseSwapWSClient,\n            subscribe_trade,\n            &[\"BTC_USD\".to_string(), \"ETH_USD\".to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook() {\n        gen_test_code!(GateInverseSwapWSClient, subscribe_orderbook, &[\"BTC_USD\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook_topk() {\n        gen_test_code!(GateInverseSwapWSClient, subscribe_orderbook_topk, &[\"BTC_USD\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_bbo() {\n        gen_test_code!(GateInverseSwapWSClient, subscribe_bbo, &[\"BTC_USD\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_ticker() {\n        gen_test_code!(GateInverseSwapWSClient, subscribe_ticker, &[\"BTC_USD\".to_string()]);\n    }\n\n    #[ignore]\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_candlestick() {\n        gen_test_subscribe_candlestick!(GateInverseSwapWSClient, &[(\"BTC_USD\".to_string(), 10)]);\n        gen_test_subscribe_candlestick!(\n            GateInverseSwapWSClient,\n            &[(\"BTC_USD\".to_string(), 604800)]\n        );\n    }\n}\n\n#[cfg(test)]\nmod gate_linear_swap {\n    use crypto_ws_client::{GateLinearSwapWSClient, WSClient};\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_trade() {\n        gen_test_code!(GateLinearSwapWSClient, subscribe_trade, &[\"BTC_USDT\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook() {\n        gen_test_code!(GateLinearSwapWSClient, subscribe_orderbook, &[\"BTC_USDT\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook_topk() {\n        gen_test_code!(GateLinearSwapWSClient, subscribe_orderbook_topk, &[\"BTC_USDT\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_bbo() {\n        gen_test_code!(GateLinearSwapWSClient, subscribe_bbo, &[\"BTC_USDT\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_ticker() {\n        gen_test_code!(GateLinearSwapWSClient, subscribe_ticker, &[\"BTC_USDT\".to_string()]);\n    }\n\n    #[ignore]\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_candlestick() {\n        gen_test_subscribe_candlestick!(GateLinearSwapWSClient, &[(\"BTC_USDT\".to_string(), 10)]);\n        gen_test_subscribe_candlestick!(\n            GateLinearSwapWSClient,\n            &[(\"BTC_USDT\".to_string(), 604800)]\n        );\n    }\n}\n\n#[cfg(test)]\nmod gate_inverse_future {\n    use crypto_ws_client::{GateInverseFutureWSClient, WSClient};\n\n    #[test]\n    #[ignore]\n    fn subscribe_trade() {\n        gen_test_code!(\n            GateInverseFutureWSClient,\n            subscribe_trade,\n            &[\"BTC_USD_20221230\".to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook() {\n        gen_test_code!(\n            GateInverseFutureWSClient,\n            subscribe_orderbook,\n            &[\"BTC_USD_20221230\".to_string()]\n        );\n    }\n\n    #[ignore = \"lack of liquidity\"]\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_ticker() {\n        gen_test_code!(\n            GateInverseFutureWSClient,\n            subscribe_ticker,\n            &[\"BTC_USD_20221230\".to_string(),]\n        );\n    }\n\n    #[ignore]\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_candlestick() {\n        gen_test_subscribe_candlestick!(\n            GateInverseFutureWSClient,\n            &[(\"BTC_USD_20221230\".to_string(), 10)]\n        );\n        gen_test_subscribe_candlestick!(\n            GateInverseFutureWSClient,\n            &[(\"BTC_USD_20221230\".to_string(), 604800)]\n        );\n    }\n}\n\n#[cfg(test)]\nmod gate_linear_future {\n    use crypto_ws_client::{GateLinearFutureWSClient, WSClient};\n\n    #[test]\n    #[ignore]\n    fn subscribe_trade() {\n        gen_test_code!(\n            GateLinearFutureWSClient,\n            subscribe_trade,\n            &[\"BTC_USDT_20221230\".to_string()]\n        );\n    }\n\n    #[test]\n    #[ignore]\n    fn subscribe_orderbook() {\n        gen_test_code!(\n            GateLinearFutureWSClient,\n            subscribe_orderbook,\n            &[\"BTC_USDT_20221230\".to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_ticker() {\n        gen_test_code!(\n            GateLinearFutureWSClient,\n            subscribe_ticker,\n            &[\"BTC_USDT_20221230\".to_string()]\n        );\n    }\n\n    #[ignore]\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_candlestick() {\n        gen_test_subscribe_candlestick!(\n            GateLinearFutureWSClient,\n            &[(\"BTC_USDT_20221230\".to_string(), 10)]\n        );\n        gen_test_subscribe_candlestick!(\n            GateLinearFutureWSClient,\n            &[(\"BTC_USDT_20221230\".to_string(), 604800)]\n        );\n    }\n}\n"
  },
  {
    "path": "crypto-ws-client/tests/huobi.rs",
    "content": "#[macro_use]\nmod utils;\n\n#[cfg(test)]\nmod huobi_spot {\n    use crypto_ws_client::{HuobiSpotWSClient, WSClient};\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe() {\n        gen_test_code!(\n            HuobiSpotWSClient,\n            subscribe,\n            &[(\"trade.detail\".to_string(), \"btcusdt\".to_string())]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_raw_json() {\n        gen_test_code!(\n            HuobiSpotWSClient,\n            send,\n            &[r#\"{\"sub\":\"market.btcusdt.trade.detail\",\"id\":\"crypto-ws-client\"}\"#.to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_trade() {\n        gen_test_code!(HuobiSpotWSClient, subscribe_trade, &[\"btcusdt\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_ticker() {\n        gen_test_code!(HuobiSpotWSClient, subscribe_ticker, &[\"btcusdt\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_bbo() {\n        gen_test_code!(HuobiSpotWSClient, subscribe_bbo, &[\"btcusdt\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook() {\n        let (tx, rx) = std::sync::mpsc::channel();\n        tokio::task::spawn(async move {\n            let ws_client = HuobiSpotWSClient::new(tx, Some(\"wss://api.huobi.pro/feed\")).await;\n            ws_client.subscribe_orderbook(&[\"btcusdt\".to_string()]).await;\n            // run for 60 seconds at most\n            let _ = tokio::time::timeout(std::time::Duration::from_secs(60), ws_client.run()).await;\n            ws_client.close().await;\n        });\n\n        rx.into_iter().next().expect(\"should has at least 1 element\");\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook_topk() {\n        gen_test_code!(HuobiSpotWSClient, subscribe_orderbook_topk, &[\"btcusdt\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_candlestick() {\n        gen_test_subscribe_candlestick!(HuobiSpotWSClient, &[(\"btcusdt\".to_string(), 60)]);\n        gen_test_subscribe_candlestick!(HuobiSpotWSClient, &[(\"btcusdt\".to_string(), 2592000)]);\n    }\n}\n\n#[cfg(test)]\nmod huobi_inverse_future {\n    use crypto_ws_client::{HuobiFutureWSClient, WSClient};\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe() {\n        gen_test_code!(\n            HuobiFutureWSClient,\n            subscribe,\n            &[(\"trade.detail\".to_string(), \"BTC_CQ\".to_string())]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_trade() {\n        gen_test_code!(HuobiFutureWSClient, subscribe_trade, &[\"BTC_CQ\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_ticker() {\n        gen_test_code!(HuobiFutureWSClient, subscribe_ticker, &[\"BTC_CQ\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_bbo() {\n        gen_test_code!(HuobiFutureWSClient, subscribe_bbo, &[\"BTC_CQ\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook() {\n        gen_test_code!(HuobiFutureWSClient, subscribe_orderbook, &[\"BTC_CQ\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook_topk() {\n        gen_test_code!(HuobiFutureWSClient, subscribe_orderbook_topk, &[\"BTC_CQ\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_candlestick() {\n        gen_test_subscribe_candlestick!(HuobiFutureWSClient, &[(\"BTC_CQ\".to_string(), 60)]);\n        gen_test_subscribe_candlestick!(HuobiFutureWSClient, &[(\"BTC_CQ\".to_string(), 2592000)]);\n    }\n}\n\n#[cfg(test)]\nmod huobi_linear_swap {\n    use crypto_ws_client::{HuobiLinearSwapWSClient, WSClient};\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe() {\n        gen_test_code!(\n            HuobiLinearSwapWSClient,\n            subscribe,\n            &[(\"trade.detail\".to_string(), \"BTC-USDT\".to_string())]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_trade() {\n        gen_test_code!(HuobiLinearSwapWSClient, subscribe_trade, &[\"BTC-USDT\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_ticker() {\n        gen_test_code!(HuobiLinearSwapWSClient, subscribe_ticker, &[\"BTC-USDT\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_bbo() {\n        gen_test_code!(HuobiLinearSwapWSClient, subscribe_bbo, &[\"BTC-USDT\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook() {\n        gen_test_code!(HuobiLinearSwapWSClient, subscribe_orderbook, &[\"BTC-USDT\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook_topk() {\n        gen_test_code!(\n            HuobiLinearSwapWSClient,\n            subscribe_orderbook_topk,\n            &[\"BTC-USDT\".to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_candlestick() {\n        gen_test_subscribe_candlestick!(HuobiLinearSwapWSClient, &[(\"BTC-USDT\".to_string(), 60)]);\n        gen_test_subscribe_candlestick!(\n            HuobiLinearSwapWSClient,\n            &[(\"BTC-USDT\".to_string(), 2592000)]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_funding_rate() {\n        let (tx, rx) = std::sync::mpsc::channel();\n        tokio::task::spawn(async move {\n            let ws_client = HuobiLinearSwapWSClient::new(\n                tx,\n                Some(\"wss://api.hbdm.com/linear-swap-notification\"),\n            )\n            .await;\n            ws_client\n                .send(&[r#\"{\"topic\":\"public.BTC-USDT.funding_rate\",\"op\":\"sub\"}\"#.to_string()])\n                .await;\n            // run for 60 seconds at most\n            let _ = tokio::time::timeout(std::time::Duration::from_secs(60), ws_client.run()).await;\n            ws_client.close().await;\n        });\n\n        rx.into_iter().next().expect(\"should has at least 1 element\");\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_funding_rate_all() {\n        let (tx, rx) = std::sync::mpsc::channel();\n        tokio::task::spawn(async move {\n            let ws_client = HuobiLinearSwapWSClient::new(\n                tx,\n                Some(\"wss://api.hbdm.com/linear-swap-notification\"),\n            )\n            .await;\n            ws_client.send(&[r#\"{\"topic\":\"public.*.funding_rate\",\"op\":\"sub\"}\"#.to_string()]).await;\n            // run for 60 seconds at most\n            let _ = tokio::time::timeout(std::time::Duration::from_secs(60), ws_client.run()).await;\n            ws_client.close().await;\n        });\n\n        rx.into_iter().next().expect(\"should has at least 1 element\");\n    }\n}\n\n#[cfg(test)]\nmod huobi_inverse_swap {\n    use crypto_ws_client::{HuobiInverseSwapWSClient, WSClient};\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe() {\n        gen_test_code!(\n            HuobiInverseSwapWSClient,\n            subscribe,\n            &[(\"trade.detail\".to_string(), \"BTC-USD\".to_string())]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_trade() {\n        gen_test_code!(HuobiInverseSwapWSClient, subscribe_trade, &[\"BTC-USD\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_ticker() {\n        gen_test_code!(HuobiInverseSwapWSClient, subscribe_ticker, &[\"BTC-USD\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_bbo() {\n        gen_test_code!(HuobiInverseSwapWSClient, subscribe_bbo, &[\"BTC-USD\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook() {\n        gen_test_code!(HuobiInverseSwapWSClient, subscribe_orderbook, &[\"BTC-USD\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook_topk() {\n        gen_test_code!(\n            HuobiInverseSwapWSClient,\n            subscribe_orderbook_topk,\n            &[\"BTC-USD\".to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_candlestick() {\n        gen_test_subscribe_candlestick!(HuobiInverseSwapWSClient, &[(\"BTC-USD\".to_string(), 60)]);\n        gen_test_subscribe_candlestick!(\n            HuobiInverseSwapWSClient,\n            &[(\"BTC-USD\".to_string(), 2592000)]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_funding_rate() {\n        let (tx, rx) = std::sync::mpsc::channel();\n        tokio::task::spawn(async move {\n            let ws_client =\n                HuobiInverseSwapWSClient::new(tx, Some(\"wss://api.hbdm.com/swap-notification\"))\n                    .await;\n            ws_client\n                .send(&[r#\"{\"topic\":\"public.BTC-USD.funding_rate\",\"op\":\"sub\"}\"#.to_string()])\n                .await;\n            // run for 60 seconds at most\n            let _ = tokio::time::timeout(std::time::Duration::from_secs(60), ws_client.run()).await;\n            ws_client.close().await;\n        });\n\n        rx.into_iter().next().expect(\"should has at least 1 element\");\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_funding_rate_all() {\n        let (tx, rx) = std::sync::mpsc::channel();\n        tokio::task::spawn(async move {\n            let ws_client =\n                HuobiInverseSwapWSClient::new(tx, Some(\"wss://api.hbdm.com/swap-notification\"))\n                    .await;\n            ws_client.send(&[r#\"{\"topic\":\"public.*.funding_rate\",\"op\":\"sub\"}\"#.to_string()]).await;\n            // run for 60 seconds at most\n            let _ = tokio::time::timeout(std::time::Duration::from_secs(60), ws_client.run()).await;\n            ws_client.close().await;\n        });\n\n        rx.into_iter().next().expect(\"should has at least 1 element\");\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_overview() {\n        gen_test_code!(\n            HuobiInverseSwapWSClient,\n            send,\n            &[r#\"{\"sub\":\"market.overview\",\"id\":\"crypto-ws-client\"}\"#.to_string()]\n        );\n    }\n}\n\n#[cfg(test)]\nmod huobi_option {\n    use crypto_ws_client::{HuobiOptionWSClient, WSClient};\n\n    #[test]\n    #[ignore]\n    fn subscribe() {\n        gen_test_code!(\n            HuobiOptionWSClient,\n            subscribe,\n            &[(\"trade.detail\".to_string(), \"BTC-USDT-210625-P-27000\".to_string())]\n        );\n    }\n\n    #[test]\n    #[ignore]\n    fn subscribe_trade() {\n        gen_test_code!(\n            HuobiOptionWSClient,\n            subscribe_trade,\n            &[\"BTC-USDT-210625-P-27000\".to_string()]\n        );\n    }\n\n    #[test]\n    #[ignore]\n    fn subscribe_ticker() {\n        gen_test_code!(\n            HuobiOptionWSClient,\n            subscribe_ticker,\n            &[\"BTC-USDT-210625-P-27000\".to_string()]\n        );\n    }\n\n    #[test]\n    #[ignore]\n    fn subscribe_bbo() {\n        gen_test_code!(\n            HuobiOptionWSClient,\n            subscribe_bbo,\n            &[\"BTC-USDT-210625-P-27000\".to_string()]\n        );\n    }\n\n    #[test]\n    #[ignore]\n    fn subscribe_orderbook() {\n        gen_test_code!(\n            HuobiOptionWSClient,\n            subscribe_orderbook,\n            &[\"BTC-USDT-210625-P-27000\".to_string()]\n        );\n    }\n\n    #[test]\n    #[ignore]\n    fn subscribe_orderbook_topk() {\n        gen_test_code!(\n            HuobiOptionWSClient,\n            subscribe_orderbook_topk,\n            &[\"BTC-USDT-210625-P-27000\".to_string()]\n        );\n    }\n\n    #[test]\n    #[ignore]\n    fn subscribe_candlestick() {\n        gen_test_subscribe_candlestick!(\n            HuobiOptionWSClient,\n            &[(\"BTC-USDT-210625-P-27000\".to_string(), 60)]\n        );\n        gen_test_subscribe_candlestick!(\n            HuobiOptionWSClient,\n            &[(\"BTC-USDT-210625-P-27000\".to_string(), 2592000)]\n        );\n    }\n\n    #[test]\n    #[ignore]\n    fn subscribe_overview() {\n        gen_test_code!(\n            HuobiOptionWSClient,\n            send,\n            &[r#\"{\"sub\":\"market.overview\",\"id\":\"crypto-ws-client\"}\"#.to_string()]\n        );\n    }\n}\n"
  },
  {
    "path": "crypto-ws-client/tests/kraken.rs",
    "content": "#[macro_use]\nmod utils;\n\n#[cfg(test)]\nmod kraken_spot {\n    use crypto_ws_client::{KrakenSpotWSClient, WSClient};\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe() {\n        gen_test_code!(\n            KrakenSpotWSClient,\n            subscribe,\n            &[\n                (\"trade\".to_string(), \"XBT/USD\".to_string()),\n                (\"ticker\".to_string(), \"XBT/USD\".to_string()),\n                (\"spread\".to_string(), \"XBT/USD\".to_string()),\n                (\"book\".to_string(), \"XBT/USD\".to_string())\n            ]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_raw_json() {\n        gen_test_code!(\n            KrakenSpotWSClient,\n            send,\n            &[r#\"{\"event\":\"subscribe\",\"pair\":[\"XBT/USD\"],\"subscription\":{\"name\":\"trade\"}}\"#\n                .to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_trade() {\n        gen_test_code!(\n            KrakenSpotWSClient,\n            subscribe_trade,\n            &[\"XBT/USD\".to_string(), \"ETH/USD\".to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_ticker() {\n        gen_test_code!(\n            KrakenSpotWSClient,\n            subscribe_ticker,\n            &[\"XBT/USD\".to_string(), \"ETH/USD\".to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_bbo() {\n        gen_test_code!(\n            KrakenSpotWSClient,\n            subscribe_bbo,\n            &[\"XBT/USD\".to_string(), \"ETH/USD\".to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook() {\n        gen_test_code!(\n            KrakenSpotWSClient,\n            subscribe_orderbook,\n            &[\"XBT/USD\".to_string(), \"ETH/USD\".to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_candlestick() {\n        gen_test_subscribe_candlestick!(\n            KrakenSpotWSClient,\n            &[(\"XBT/USD\".to_string(), 60), (\"ETH/USD\".to_string(), 60)]\n        );\n\n        gen_test_subscribe_candlestick!(\n            KrakenSpotWSClient,\n            &[(\"XBT/USD\".to_string(), 1296000), (\"ETH/USD\".to_string(), 1296000)]\n        );\n    }\n}\n\n#[cfg(test)]\nmod kraken_inverse_swap {\n    use crypto_ws_client::{KrakenFuturesWSClient, WSClient};\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_raw_json() {\n        gen_test_code!(\n            KrakenFuturesWSClient,\n            send,\n            &[r#\"{\"event\":\"subscribe\",\"feed\":\"trade\",\"product_ids\":[\"PI_XBTUSD\"]}\"#.to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_trade() {\n        gen_test_code!(KrakenFuturesWSClient, subscribe_trade, &[\"PI_XBTUSD\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_ticker() {\n        gen_test_code!(KrakenFuturesWSClient, subscribe_ticker, &[\"PI_XBTUSD\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook() {\n        gen_test_code!(KrakenFuturesWSClient, subscribe_orderbook, &[\"PI_XBTUSD\".to_string()]);\n    }\n}\n\n#[cfg(test)]\nmod kraken_inverse_future {\n    use crypto_ws_client::{KrakenFuturesWSClient, WSClient};\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_raw_json() {\n        gen_test_code!(\n            KrakenFuturesWSClient,\n            send,\n            &[r#\"{\"event\":\"subscribe\",\"feed\":\"trade\",\"product_ids\":[\"FI_XBTUSD_221230\"]}\"#\n                .to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_trade() {\n        gen_test_code!(KrakenFuturesWSClient, subscribe_trade, &[\"FI_XBTUSD_221230\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_ticker() {\n        gen_test_code!(KrakenFuturesWSClient, subscribe_ticker, &[\"FI_XBTUSD_221230\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook() {\n        gen_test_code!(\n            KrakenFuturesWSClient,\n            subscribe_orderbook,\n            &[\"FI_XBTUSD_221230\".to_string()]\n        );\n    }\n}\n"
  },
  {
    "path": "crypto-ws-client/tests/kucoin.rs",
    "content": "#[macro_use]\nmod utils;\n\n#[cfg(test)]\nmod kucoin_spot {\n    use crypto_ws_client::{KuCoinSpotWSClient, WSClient};\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe() {\n        gen_test_code!(\n            KuCoinSpotWSClient,\n            subscribe,\n            &[(\"/market/match\".to_string(), \"BTC-USDT\".to_string())]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_all_bbo() {\n        gen_test_code!(\n            KuCoinSpotWSClient,\n            subscribe,\n            &[(\"/market/ticker\".to_string(), \"all\".to_string())]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_raw_json() {\n        gen_test_code!(\n            KuCoinSpotWSClient,\n            send,\n            &[r#\"{\"id\":\"crypto-ws-client\",\"type\":\"subscribe\",\"topic\":\"/market/match:BTC-USDT\",\"privateChannel\":false,\"response\":true}\"#.to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_trade() {\n        gen_test_code!(\n            KuCoinSpotWSClient,\n            subscribe_trade,\n            &[\"BTC-USDT\".to_string(), \"ETH-USDT\".to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_bbo() {\n        gen_test_code!(KuCoinSpotWSClient, subscribe_bbo, &[\"BTC-USDT\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook() {\n        gen_test_code!(KuCoinSpotWSClient, subscribe_orderbook, &[\"BTC-USDT\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook_topk() {\n        gen_test_code!(KuCoinSpotWSClient, subscribe_orderbook_topk, &[\"BTC-USDT\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_ticker() {\n        gen_test_code!(KuCoinSpotWSClient, subscribe_ticker, &[\"BTC-USDT\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_candlestick() {\n        gen_test_subscribe_candlestick!(\n            KuCoinSpotWSClient,\n            &[(\"BTC-USDT\".to_string(), 60), (\"BTC-USDT\".to_string(), 604800)]\n        );\n    }\n}\n\n#[cfg(test)]\nmod kucoin_inverse_swap {\n    use crypto_ws_client::{KuCoinSwapWSClient, WSClient};\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe() {\n        gen_test_code!(\n            KuCoinSwapWSClient,\n            subscribe,\n            &[\n                (\"/contractMarket/execution\".to_string(), \"XBTUSDM\".to_string()),\n                (\"/contractMarket/execution\".to_string(), \"ETHUSDM\".to_string()),\n                (\"/contractMarket/execution\".to_string(), \"DOTUSDM\".to_string()),\n                (\"/contractMarket/execution\".to_string(), \"XRPUSDM\".to_string())\n            ]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_raw_json() {\n        gen_test_code!(\n            KuCoinSwapWSClient,\n            send,\n            &[r#\"{\"id\":\"crypto-ws-client\",\"type\":\"subscribe\",\"topic\":\"/contractMarket/execution:XBTUSDM,ETHUSDM,DOTUSDM,XRPUSDM\",\"privateChannel\":false,\"response\":true}\"#.to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_trade() {\n        gen_test_code!(\n            KuCoinSwapWSClient,\n            subscribe_trade,\n            &[\n                \"XBTUSDM\".to_string(),\n                \"ETHUSDM\".to_string(),\n                \"DOTUSDM\".to_string(),\n                \"XRPUSDM\".to_string()\n            ]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_bbo() {\n        gen_test_code!(KuCoinSwapWSClient, subscribe_bbo, &[\"XBTUSDM\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook() {\n        gen_test_code!(KuCoinSwapWSClient, subscribe_orderbook, &[\"XBTUSDM\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook_topk() {\n        gen_test_code!(KuCoinSwapWSClient, subscribe_orderbook_topk, &[\"XBTUSDM\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_ticker() {\n        gen_test_code!(KuCoinSwapWSClient, subscribe_ticker, &[\"XBTUSDM\".to_string()]);\n    }\n\n    #[ignore]\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_candlestick() {\n        gen_test_subscribe_candlestick!(\n            KuCoinSwapWSClient,\n            &[\n                (\"XBTUSDM\".to_string(), 60),\n                (\"ETHUSDM\".to_string(), 60),\n                (\"XBTUSDM\".to_string(), 604800),\n                (\"ETHUSDM\".to_string(), 604800)\n            ]\n        );\n    }\n}\n\n#[cfg(test)]\nmod kucoin_linear_swap {\n    use crypto_ws_client::{KuCoinSwapWSClient, WSClient};\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe() {\n        gen_test_code!(\n            KuCoinSwapWSClient,\n            subscribe,\n            &[(\"/contractMarket/execution\".to_string(), \"XBTUSDTM\".to_string())]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_raw_json() {\n        gen_test_code!(\n            KuCoinSwapWSClient,\n            send,\n            &[r#\"{\"id\":\"crypto-ws-client\",\"type\":\"subscribe\",\"topic\":\"/contractMarket/execution:XBTUSDTM\",\"privateChannel\":false,\"response\":true}\"#.to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_trade() {\n        gen_test_code!(KuCoinSwapWSClient, subscribe_trade, &[\"XBTUSDTM\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_bbo() {\n        gen_test_code!(KuCoinSwapWSClient, subscribe_bbo, &[\"XBTUSDTM\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook() {\n        gen_test_code!(KuCoinSwapWSClient, subscribe_orderbook, &[\"XBTUSDTM\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook_topk() {\n        gen_test_code!(KuCoinSwapWSClient, subscribe_orderbook_topk, &[\"XBTUSDTM\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_ticker() {\n        gen_test_code!(KuCoinSwapWSClient, subscribe_ticker, &[\"XBTUSDTM\".to_string()]);\n    }\n\n    #[ignore]\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_candlestick() {\n        gen_test_subscribe_candlestick!(\n            KuCoinSwapWSClient,\n            &[\n                (\"XBTUSDTM\".to_string(), 60),\n                (\"ETHUSDTM\".to_string(), 60),\n                (\"XBTUSDTM\".to_string(), 604800),\n                (\"ETHUSDTM\".to_string(), 604800)\n            ]\n        );\n    }\n}\n\n#[cfg(test)]\nmod kucoin_inverse_future {\n    use crypto_ws_client::{KuCoinSwapWSClient, WSClient};\n\n    #[ignore]\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe() {\n        gen_test_code!(\n            KuCoinSwapWSClient,\n            subscribe,\n            &[(\"/contractMarket/execution\".to_string(), \"XBTMZ22\".to_string())]\n        );\n    }\n\n    #[ignore]\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_raw_json() {\n        gen_test_code!(\n            KuCoinSwapWSClient,\n            send,\n            &[r#\"{\"id\":\"crypto-ws-client\",\"type\":\"subscribe\",\"topic\":\"/contractMarket/execution:XBTMZ22\",\"privateChannel\":false,\"response\":true}\"#.to_string()]\n        );\n    }\n\n    #[ignore]\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_trade() {\n        gen_test_code!(KuCoinSwapWSClient, subscribe_trade, &[\"XBTMZ22\".to_string()]);\n    }\n\n    #[ignore]\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_bbo() {\n        gen_test_code!(KuCoinSwapWSClient, subscribe_bbo, &[\"XBTMZ22\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook() {\n        gen_test_code!(KuCoinSwapWSClient, subscribe_orderbook, &[\"XBTMZ22\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook_topk() {\n        gen_test_code!(KuCoinSwapWSClient, subscribe_orderbook_topk, &[\"XBTMZ22\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_ticker() {\n        gen_test_code!(KuCoinSwapWSClient, subscribe_ticker, &[\"XBTMZ22\".to_string()]);\n    }\n\n    #[ignore]\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_candlestick() {\n        gen_test_subscribe_candlestick!(\n            KuCoinSwapWSClient,\n            &[(\"XBTMZ22\".to_string(), 60), (\"XBTMZ22\".to_string(), 604800)]\n        );\n    }\n}\n"
  },
  {
    "path": "crypto-ws-client/tests/mexc.rs",
    "content": "#[macro_use]\nmod utils;\n\n#[cfg(test)]\nmod mexc_spot {\n    use crypto_ws_client::{MexcSpotWSClient, WSClient};\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe() {\n        gen_test_code!(\n            MexcSpotWSClient,\n            subscribe,\n            &[\n                (\"deal\".to_string(), \"BTC_USDT\".to_string()),\n                (\"deal\".to_string(), \"ETH_USDT\".to_string()),\n                (\"deal\".to_string(), \"MX_USDT\".to_string())\n            ]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_raw_json() {\n        gen_test_code!(\n            MexcSpotWSClient,\n            send,\n            &[\n                r#\"{\"op\":\"sub.deal\",\"symbol\":\"BTC_USDT\"}\"#.to_string(),\n                r#\"{\"op\":\"sub.deal\",\"symbol\":\"ETH_USDT\"}\"#.to_string(),\n                r#\"{\"op\":\"sub.deal\",\"symbol\":\"MX_USDT\"}\"#.to_string()\n            ]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_trade() {\n        gen_test_code!(\n            MexcSpotWSClient,\n            subscribe_trade,\n            &[\"BTC_USDT\".to_string(), \"ETH_USDT\".to_string(), \"MX_USDT\".to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook() {\n        gen_test_code!(\n            MexcSpotWSClient,\n            subscribe_orderbook,\n            &[\"BTC_USDT\".to_string(), \"ETH_USDT\".to_string(), \"MX_USDT\".to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook_topk() {\n        gen_test_code!(MexcSpotWSClient, subscribe_orderbook_topk, &[\"BTC_USDT\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_candlestick() {\n        gen_test_subscribe_candlestick!(\n            MexcSpotWSClient,\n            &[\n                (\"BTC_USDT\".to_string(), 60),\n                (\"ETH_USDT\".to_string(), 60),\n                (\"MX_USDT\".to_string(), 60)\n            ]\n        );\n        gen_test_subscribe_candlestick!(\n            MexcSpotWSClient,\n            &[\n                (\"BTC_USDT\".to_string(), 2592000),\n                (\"ETH_USDT\".to_string(), 2592000),\n                (\"MX_USDT\".to_string(), 2592000)\n            ]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_overview() {\n        gen_test_code!(MexcSpotWSClient, send, &[r#\"{\"op\":\"sub.overview\"}\"#.to_string()]);\n    }\n}\n\n#[cfg(test)]\nmod mexc_linear_swap {\n    use crypto_ws_client::{MexcSwapWSClient, WSClient};\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe() {\n        gen_test_code!(\n            MexcSwapWSClient,\n            subscribe,\n            &[\n                (\"deal\".to_string(), \"BTC_USDT\".to_string()),\n                (\"deal\".to_string(), \"ETH_USDT\".to_string())\n            ]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_raw_json() {\n        gen_test_code!(\n            MexcSwapWSClient,\n            send,\n            &[r#\"{\"method\":\"sub.deal\",\"param\":{\"symbol\":\"BTC_USDT\"}}\"#.to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_trade() {\n        gen_test_code!(MexcSwapWSClient, subscribe_trade, &[\"BTC_USDT\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_ticker() {\n        gen_test_code!(MexcSwapWSClient, subscribe_ticker, &[\"BTC_USDT\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook() {\n        gen_test_code!(MexcSwapWSClient, subscribe_orderbook, &[\"BTC_USDT\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook_topk() {\n        gen_test_code!(MexcSwapWSClient, subscribe_orderbook_topk, &[\"BTC_USDT\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_candlestick() {\n        gen_test_subscribe_candlestick!(MexcSwapWSClient, &[(\"BTC_USDT\".to_string(), 60)]);\n        gen_test_subscribe_candlestick!(MexcSwapWSClient, &[(\"BTC_USDT\".to_string(), 2592000)]);\n    }\n}\n\n#[cfg(test)]\nmod mexc_inverse_swap {\n    use crypto_ws_client::{MexcSwapWSClient, WSClient};\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_trade() {\n        gen_test_code!(MexcSwapWSClient, subscribe_trade, &[\"BTC_USD\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_ticker() {\n        gen_test_code!(MexcSwapWSClient, subscribe_ticker, &[\"BTC_USD\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook() {\n        gen_test_code!(MexcSwapWSClient, subscribe_orderbook, &[\"BTC_USD\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook_topk() {\n        gen_test_code!(MexcSwapWSClient, subscribe_orderbook_topk, &[\"BTC_USD\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_candlestick() {\n        gen_test_subscribe_candlestick!(MexcSwapWSClient, &[(\"BTC_USD\".to_string(), 60)]);\n        gen_test_subscribe_candlestick!(MexcSwapWSClient, &[(\"BTC_USD\".to_string(), 2592000)]);\n    }\n}\n"
  },
  {
    "path": "crypto-ws-client/tests/okx.rs",
    "content": "use crypto_ws_client::{OkxWSClient, WSClient};\n\n#[macro_use]\nmod utils;\n\n#[tokio::test(flavor = \"multi_thread\")]\nasync fn okex_index() {\n    gen_test_code!(\n        OkxWSClient,\n        subscribe,\n        &[(\"index-tickers\".to_string(), \"BTC-USDT\".to_string())]\n    );\n}\n\n#[cfg(test)]\nmod okx_spot {\n    use crypto_ws_client::{OkxWSClient, WSClient};\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe() {\n        gen_test_code!(OkxWSClient, subscribe, &[(\"trades\".to_string(), \"BTC-USDT\".to_string())]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_raw_json() {\n        gen_test_code!(\n            OkxWSClient,\n            send,\n            &[r#\"{\"op\":\"subscribe\",\"args\":[{\"channel\":\"trades\",\"instId\":\"BTC-USDT\"}]}\"#\n                .to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_trade() {\n        gen_test_code!(OkxWSClient, subscribe_trade, &[\"BTC-USDT\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_ticker() {\n        gen_test_code!(OkxWSClient, subscribe_ticker, &[\"BTC-USDT\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_bbo() {\n        gen_test_code!(OkxWSClient, subscribe_bbo, &[\"BTC-USDT\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook() {\n        gen_test_code!(OkxWSClient, subscribe_orderbook, &[\"BTC-USDT\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook_topk() {\n        gen_test_code!(OkxWSClient, subscribe_orderbook_topk, &[\"BTC-USDT\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_candlestick() {\n        gen_test_subscribe_candlestick!(OkxWSClient, &[(\"BTC-USDT\".to_string(), 60)]);\n        gen_test_subscribe_candlestick!(OkxWSClient, &[(\"BTC-USDT\".to_string(), 604800)]);\n    }\n}\n\n#[cfg(test)]\nmod okx_future {\n    use crypto_ws_client::{OkxWSClient, WSClient};\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe() {\n        gen_test_code!(\n            OkxWSClient,\n            subscribe,\n            &[(\"trades\".to_string(), \"BTC-USDT-221230\".to_string())]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_trade() {\n        gen_test_code!(OkxWSClient, subscribe_trade, &[\"BTC-USDT-221230\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_ticker() {\n        gen_test_code!(OkxWSClient, subscribe_ticker, &[\"BTC-USDT-221230\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_bbo() {\n        gen_test_code!(OkxWSClient, subscribe_bbo, &[\"BTC-USDT-221230\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook() {\n        gen_test_code!(OkxWSClient, subscribe_orderbook, &[\"BTC-USDT-221230\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook_topk() {\n        gen_test_code!(OkxWSClient, subscribe_orderbook_topk, &[\"BTC-USDT-221230\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_candlestick() {\n        gen_test_subscribe_candlestick!(OkxWSClient, &[(\"BTC-USDT-221230\".to_string(), 60)]);\n        gen_test_subscribe_candlestick!(OkxWSClient, &[(\"BTC-USDT-221230\".to_string(), 604800)]);\n    }\n}\n\n#[cfg(test)]\nmod okx_swap {\n    use crypto_ws_client::{OkxWSClient, WSClient};\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe() {\n        gen_test_code!(\n            OkxWSClient,\n            subscribe,\n            &[(\"trades\".to_string(), \"BTC-USDT-SWAP\".to_string())]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_trade() {\n        gen_test_code!(OkxWSClient, subscribe_trade, &[\"BTC-USDT-SWAP\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_ticker() {\n        gen_test_code!(OkxWSClient, subscribe_ticker, &[\"BTC-USDT-SWAP\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_bbo() {\n        gen_test_code!(OkxWSClient, subscribe_bbo, &[\"BTC-USDT-SWAP\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook() {\n        gen_test_code!(OkxWSClient, subscribe_orderbook, &[\"BTC-USDT-SWAP\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook_topk() {\n        gen_test_code!(OkxWSClient, subscribe_orderbook_topk, &[\"BTC-USDT-SWAP\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_candlestick() {\n        gen_test_subscribe_candlestick!(OkxWSClient, &[(\"BTC-USDT-SWAP\".to_string(), 60)]);\n        gen_test_subscribe_candlestick!(OkxWSClient, &[(\"BTC-USDT-SWAP\".to_string(), 604800)]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_funding_rate() {\n        gen_test_code!(\n            OkxWSClient,\n            subscribe,\n            &[(\"funding-rate\".to_string(), \"BTC-USDT-SWAP\".to_string())]\n        );\n    }\n}\n\n#[cfg(test)]\nmod okx_option {\n    use crypto_ws_client::{OkxWSClient, WSClient};\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    #[ignore = \"lack of liquidity\"]\n    async fn subscribe_trade() {\n        gen_test_code!(OkxWSClient, subscribe_trade, &[\"BTC-USD-221230-50000-C\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_ticker() {\n        gen_test_code!(OkxWSClient, subscribe_ticker, &[\"BTC-USD-221230-50000-C\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_bbo() {\n        gen_test_code!(OkxWSClient, subscribe_bbo, &[\"BTC-USD-221230-50000-C\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook() {\n        gen_test_code!(OkxWSClient, subscribe_orderbook, &[\"BTC-USD-221230-50000-C\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook_topk() {\n        gen_test_code!(\n            OkxWSClient,\n            subscribe_orderbook_topk,\n            &[\"BTC-USD-221230-50000-C\".to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    #[ignore = \"lack of liquidity\"]\n    async fn subscribe_candlestick() {\n        gen_test_subscribe_candlestick!(OkxWSClient, &[(\"BTC-USD-221230-50000-C\".to_string(), 60)]);\n        gen_test_subscribe_candlestick!(\n            OkxWSClient,\n            &[(\"BTC-USD-221230-50000-C\".to_string(), 604800)]\n        );\n    }\n}\n"
  },
  {
    "path": "crypto-ws-client/tests/utils/mod.rs",
    "content": "macro_rules! gen_test_code {\n    ($client:ident, $func_name:ident, $symbols:expr) => {\n        let (tx, rx) = std::sync::mpsc::channel();\n        tokio::task::spawn(async move {\n            let ws_client = $client::new(tx, None).await;\n            ws_client.$func_name($symbols).await;\n            // run for 60 seconds at most\n            let _ = tokio::time::timeout(std::time::Duration::from_secs(60), ws_client.run()).await;\n            ws_client.close().await;\n        });\n\n        let mut messages = Vec::<String>::new();\n        for msg in rx {\n            messages.push(msg);\n            break;\n        }\n        assert!(!messages.is_empty());\n    };\n}\n\n#[allow(unused_macros)]\nmacro_rules! gen_test_subscribe_candlestick {\n    ($client:ident, $symbol_interval_list:expr) => {\n        let (tx, rx) = std::sync::mpsc::channel();\n        tokio::task::spawn(async move {\n            let ws_client = $client::new(tx, None).await;\n            ws_client.subscribe_candlestick($symbol_interval_list).await;\n            // run for 60 seconds at most\n            let _ = tokio::time::timeout(std::time::Duration::from_secs(60), ws_client.run()).await;\n            ws_client.close().await;\n        });\n\n        let mut messages = Vec::<String>::new();\n        for msg in rx {\n            messages.push(msg);\n            break;\n        }\n        assert!(!messages.is_empty());\n    };\n}\n"
  },
  {
    "path": "crypto-ws-client/tests/zb.rs",
    "content": "#[macro_use]\nmod utils;\n\n#[cfg(test)]\nmod zb_spot {\n    use crypto_ws_client::{WSClient, ZbSpotWSClient};\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe() {\n        gen_test_code!(\n            ZbSpotWSClient,\n            subscribe,\n            &[(\"trades\".to_string(), \"btc_usdt\".to_string())]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_raw_json() {\n        gen_test_code!(\n            ZbSpotWSClient,\n            send,\n            &[r#\"{\"event\":\"addChannel\",\"channel\":\"btcusdt_trades\"}\"#.to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_trade() {\n        gen_test_code!(ZbSpotWSClient, subscribe_trade, &[\"btc_usdt\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook_topk() {\n        gen_test_code!(ZbSpotWSClient, subscribe_orderbook_topk, &[\"btc_usdt\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_ticker() {\n        gen_test_code!(ZbSpotWSClient, subscribe_ticker, &[\"btc_usdt\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_candlestick() {\n        gen_test_subscribe_candlestick!(ZbSpotWSClient, &[(\"btc_usdt\".to_string(), 60)]);\n        gen_test_subscribe_candlestick!(ZbSpotWSClient, &[(\"btc_usdt\".to_string(), 604800)]);\n    }\n}\n\n#[cfg(test)]\nmod zb_linear_swap {\n    use crypto_ws_client::{WSClient, ZbSwapWSClient};\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe() {\n        gen_test_code!(\n            ZbSwapWSClient,\n            subscribe,\n            &[\n                (\"Trade\".to_string(), \"BTC_USDT\".to_string()),\n                (\"Depth\".to_string(), \"BTC_USDT\".to_string())\n            ]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_raw_json() {\n        gen_test_code!(\n            ZbSwapWSClient,\n            send,\n            &[\n                r#\"{\"action\":\"subscribe\", \"channel\":\"BTC_USDT.Trade\", \"size\":100}\"#.to_string(),\n                r#\"{\"action\":\"subscribe\", \"channel\":\"BTC_USDT.Depth\", \"size\":200}\"#.to_string()\n            ]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_trade() {\n        gen_test_code!(\n            ZbSwapWSClient,\n            subscribe_trade,\n            &[\"BTC_USDT\".to_string(), \"ETH_USDT\".to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook() {\n        gen_test_code!(\n            ZbSwapWSClient,\n            subscribe_orderbook,\n            &[\"BTC_USDT\".to_string(), \"ETH_USDT\".to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook_topk() {\n        gen_test_code!(\n            ZbSwapWSClient,\n            subscribe_orderbook_topk,\n            &[\"BTC_USDT\".to_string(), \"ETH_USDT\".to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_ticker() {\n        gen_test_code!(\n            ZbSwapWSClient,\n            subscribe_ticker,\n            &[\"BTC_USDT\".to_string(), \"ETH_USDT\".to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_candlestick() {\n        gen_test_subscribe_candlestick!(\n            ZbSwapWSClient,\n            &[(\"BTC_USDT\".to_string(), 60), (\"ETH_USDT\".to_string(), 60)]\n        );\n    }\n}\n"
  },
  {
    "path": "crypto-ws-client/tests/zbg.rs",
    "content": "#[macro_use]\nmod utils;\n\n#[cfg(test)]\nmod zbg_spot {\n    use crypto_ws_client::{WSClient, ZbgSpotWSClient};\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe() {\n        gen_test_code!(\n            ZbgSpotWSClient,\n            subscribe,\n            &[(\"TRADE\".to_string(), \"btc_usdt\".to_string())]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_raw_json() {\n        gen_test_code!(\n            ZbgSpotWSClient,\n            send,\n            &[r#\"{\"action\":\"ADD\", \"dataType\":329_TRADE_BTC_USDT}\"#.to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_trade() {\n        gen_test_code!(ZbgSpotWSClient, subscribe_trade, &[\"btc_usdt\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_orderbook() {\n        gen_test_code!(ZbgSpotWSClient, subscribe_orderbook, &[\"btc_usdt\".to_string()]);\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_ticker() {\n        gen_test_code!(ZbgSpotWSClient, subscribe_ticker, &[\"btc_usdt\".to_string()]);\n    }\n\n    #[ignore]\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_ticker_all() {\n        gen_test_code!(\n            ZbgSpotWSClient,\n            send,\n            &[r#\"{\"action\":\"ADD\", \"dataType\":\"ALL_TRADE_STATISTIC_24H\"}\"#.to_string()]\n        );\n    }\n\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_candlestick() {\n        gen_test_subscribe_candlestick!(ZbgSpotWSClient, &[(\"btc_usdt\".to_string(), 60)]);\n        gen_test_subscribe_candlestick!(ZbgSpotWSClient, &[(\"btc_usdt\".to_string(), 604800)]);\n    }\n}\n\n#[cfg(test)]\nmod zbg_inverse_swap {\n    use crypto_ws_client::{WSClient, ZbgSwapWSClient};\n\n    #[test]\n    #[ignore]\n    fn subscribe() {\n        gen_test_code!(\n            ZbgSwapWSClient,\n            subscribe,\n            &[\n                (\"future_tick\".to_string(), \"1000001\".to_string()),\n                (\"future_tick\".to_string(), \"1000003\".to_string())\n            ]\n        );\n    }\n\n    #[test]\n    #[ignore]\n    fn subscribe_raw_json() {\n        gen_test_code!(\n            ZbgSwapWSClient,\n            send,\n            &[\n                r#\"{\"action\":\"sub\", \"topic\":\"future_tick-1000001\"}\"#.to_string(),\n                r#\"{\"action\":\"sub\", \"topic\":\"future_tick-1000003\"}\"#.to_string()\n            ]\n        );\n    }\n\n    #[test]\n    #[ignore]\n    fn subscribe_trade() {\n        gen_test_code!(\n            ZbgSwapWSClient,\n            subscribe_trade,\n            &[\"BTC_USD-R\".to_string(), \"ETH_USD-R\".to_string()]\n        );\n    }\n\n    #[test]\n    #[ignore]\n    fn subscribe_orderbook() {\n        gen_test_code!(\n            ZbgSwapWSClient,\n            subscribe_orderbook,\n            &[\"BTC_USD-R\".to_string(), \"ETH_USD-R\".to_string()]\n        );\n    }\n\n    #[ignore]\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_ticker() {\n        gen_test_code!(\n            ZbgSwapWSClient,\n            subscribe_ticker,\n            &[\"BTC_USD-R\".to_string(), \"ETH_USD-R\".to_string()]\n        );\n    }\n\n    #[ignore]\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_ticker_all() {\n        gen_test_code!(\n            ZbgSwapWSClient,\n            send,\n            &[r#\"{\"action\":\"sub\", \"topic\":\"future_all_indicator\"}\"#.to_string()]\n        );\n    }\n\n    #[ignore]\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_candlestick() {\n        gen_test_subscribe_candlestick!(\n            ZbgSwapWSClient,\n            &[(\"BTC_USD-R\".to_string(), 60), (\"ETH_USD-R\".to_string(), 60)]\n        );\n        gen_test_subscribe_candlestick!(\n            ZbgSwapWSClient,\n            &[(\"BTC_USD-R\".to_string(), 604800), (\"ETH_USD-R\".to_string(), 604800)]\n        );\n    }\n}\n\n#[cfg(test)]\nmod zbg_linear_swap {\n    use crypto_ws_client::{WSClient, ZbgSwapWSClient};\n\n    #[test]\n    #[ignore]\n    fn subscribe() {\n        gen_test_code!(\n            ZbgSwapWSClient,\n            subscribe,\n            &[\n                (\"future_tick\".to_string(), \"1000000\".to_string()),\n                (\"future_tick\".to_string(), \"1000002\".to_string())\n            ]\n        );\n    }\n\n    #[test]\n    #[ignore]\n    fn subscribe_raw_json() {\n        gen_test_code!(\n            ZbgSwapWSClient,\n            send,\n            &[\n                r#\"{\"action\":\"sub\", \"topic\":\"future_tick-1000000\"}\"#.to_string(),\n                r#\"{\"action\":\"sub\", \"topic\":\"future_tick-1000002\"}\"#.to_string()\n            ]\n        );\n    }\n\n    #[test]\n    #[ignore]\n    fn subscribe_trade() {\n        gen_test_code!(\n            ZbgSwapWSClient,\n            subscribe_trade,\n            &[\"BTC_USDT\".to_string(), \"ETH_USDT\".to_string()]\n        );\n    }\n\n    #[test]\n    #[ignore]\n    fn subscribe_orderbook() {\n        gen_test_code!(\n            ZbgSwapWSClient,\n            subscribe_orderbook,\n            &[\"BTC_USDT\".to_string(), \"ETH_USDT\".to_string()]\n        );\n    }\n\n    #[ignore]\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_ticker() {\n        gen_test_code!(\n            ZbgSwapWSClient,\n            subscribe_ticker,\n            &[\"BTC_USDT\".to_string(), \"ETH_USDT\".to_string()]\n        );\n    }\n\n    #[ignore]\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_ticker_all() {\n        gen_test_code!(\n            ZbgSwapWSClient,\n            send,\n            &[r#\"{\"action\":\"sub\", \"topic\":\"future_all_indicator\"}\"#.to_string()]\n        );\n    }\n\n    #[ignore]\n    #[tokio::test(flavor = \"multi_thread\")]\n    async fn subscribe_candlestick() {\n        gen_test_subscribe_candlestick!(\n            ZbgSwapWSClient,\n            &[(\"BTC_USDT\".to_string(), 60), (\"ETH_USDT\".to_string(), 60)]\n        );\n        gen_test_subscribe_candlestick!(\n            ZbgSwapWSClient,\n            &[(\"BTC_USDT\".to_string(), 604800), (\"ETH_USDT\".to_string(), 604800)]\n        );\n    }\n}\n"
  },
  {
    "path": "rustfmt.toml",
    "content": "edition = \"2021\"\nversion = \"Two\"\nuse_small_heuristics = \"Max\"\nnewline_style = \"Unix\"\nwrap_comments = true\nformat_generated_files = false\nimports_granularity=\"Crate\"\n"
  }
]