├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README-zh_CN.md ├── README.md ├── examples ├── future_inverse_ws_api_client.rs ├── future_linear_ws_api_client.rs ├── local_orderbook.rs ├── option_ws_api_client.rs ├── private_ws_api_client.rs └── spot_ws_api_client.rs └── src ├── error.rs ├── lib.rs ├── util.rs └── ws ├── callback.rs ├── future.rs ├── mod.rs ├── option.rs ├── private.rs ├── response.rs └── spot.rs /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CI 3 | 4 | on: 5 | pull_request: 6 | push: 7 | branches: 8 | - main 9 | 10 | # This pipeline validates proposed changes. 11 | 12 | env: 13 | CARGO_TERM_COLOR: always 14 | RUST_BACKTRACE: 1 15 | 16 | jobs: 17 | cargo-check: 18 | runs-on: ${{ matrix.triple.os }} 19 | strategy: 20 | matrix: 21 | triple: 22 | - { 23 | os: "ubuntu-latest", 24 | target: "x86_64-unknown-linux-gnu", 25 | cross: false, 26 | } 27 | - { 28 | os: "macOS-latest", 29 | target: "x86_64-apple-darwin", 30 | cross: false, 31 | } 32 | # macOS ARM 33 | - { 34 | os: "macOS-latest", 35 | target: "aarch64-apple-darwin", 36 | cross: true, 37 | } 38 | steps: 39 | - name: Checkout 40 | uses: actions/checkout@v3 41 | 42 | - name: Install toolchain 43 | uses: actions-rs/toolchain@v1 44 | with: 45 | profile: minimal 46 | toolchain: stable 47 | override: true 48 | target: ${{ matrix.triple.target }} 49 | 50 | - name: Cache cargo 51 | uses: actions/cache@v3 52 | with: 53 | path: | 54 | ~/.cargo/bin/ 55 | ~/.cargo/registry/index/ 56 | ~/.cargo/registry/cache/ 57 | ~/.cargo/git/db/ 58 | target/ 59 | key: ${{ runner.os }}-${{ matrix.triple.target }}-cargo-${{ hashFiles('**/Cargo.lock') }} 60 | 61 | - name: Cargo check 62 | uses: actions-rs/cargo@v1 63 | with: 64 | command: check 65 | args: --all-targets --verbose --target=${{ matrix.triple.target }} 66 | use-cross: ${{ matrix.triple.cross }} 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rust-bybit" 3 | version = "0.2.0" 4 | license = "MIT" 5 | authors = ["jukanntenn "] 6 | edition = "2021" 7 | 8 | description = "Rust API connector for Bybit's WebSocket V5 API" 9 | keywords = ["cryptocurrency", "trading", "bybit"] 10 | categories = ["api-bindings", "cryptography::cryptocurrencies"] 11 | documentation = "https://docs.rs/crate/rust-bybit/" 12 | repository = "https://github.com/yufuquant/rust-bybit" 13 | readme = "README.md" 14 | 15 | [lib] 16 | name = "bybit" 17 | path = "src/lib.rs" 18 | 19 | [dependencies] 20 | tungstenite = { version = "0.18", features = ["native-tls"] } 21 | thiserror = "1.0" 22 | serde = { version = "1.0", features = ["derive"] } 23 | serde_json = "1.0" 24 | ring = "0.16" 25 | hex = "0.4" 26 | log = "0.4" 27 | 28 | [dev-dependencies] 29 | env_logger = "0.10" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 [yufuquant] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README-zh_CN.md: -------------------------------------------------------------------------------- 1 | # rust-bybit 2 | 3 | [![Build Status]](https://github.com/yufuquant/rust-bybit/actions/workflows/ci.yaml) 4 | 5 | [build status]: https://github.com/yufuquant/rust-bybit/actions/workflows/ci.yaml/badge.svg?branch=main 6 | 7 | [English](./README.md) | 简体中文 8 | 9 | Rust 实现的 Bybit V5 版 WebSocket 行情和交易接口**非官方** SDK。 10 | 11 | ## 免责声明 12 | 13 | 本项目是 Rust 实现的 Bybit 行情和交易接口**非官方** SDK。加密货币市场风险巨大,使用本项目前请仔细评估风险,如因使用本项目而造成的一切损失均由使用者自行承担。 14 | 15 | ## 安装 16 | 17 | 将以下依赖加到 Cargo.toml 18 | 19 | ```toml 20 | [dependencies] 21 | rust-bybit = "0.2" 22 | ``` 23 | 24 | ## 基础用法 25 | 26 | 根据需订阅的消息类型,创建对应的 client: 27 | 28 | ```rust 29 | use bybit::ws::response::SpotPublicResponse; 30 | use bybit::ws::spot; 31 | use bybit::KlineInterval; 32 | use bybit::WebSocketApiClient; 33 | 34 | let mut client = WebSocketApiClient::spot().build(); 35 | ``` 36 | 37 | 订阅感兴趣的消息。例如下面的代码将订阅 ETHUSDT 交易对(杠杆代币为 BTC3SUSDT)的全部消息(关于有哪些消息类型可供订阅,请参考 [Bybit V5 API](https://bybit-exchange.github.io/docs/zh-TW/v5/intro))。注意直到 `client.run` 被调用时才会发送订阅请求: 38 | 39 | ```rust 40 | let symbol = "ETHUSDT"; 41 | let lt_symbol = "BTC3SUSDT"; 42 | 43 | client.subscribe_orderbook(symbol, spot::OrderbookDepth::Level1); 44 | client.subscribe_orderbook(symbol, spot::OrderbookDepth::Level50); 45 | client.subscribe_trade(symbol); 46 | client.subscribe_ticker(symbol); 47 | client.subscribe_kline(symbol, KlineInterval::Min1); 48 | client.subscribe_lt_kline(lt_symbol, KlineInterval::Min5); 49 | client.subscribe_lt_ticker(lt_symbol); 50 | client.subscribe_lt_nav(lt_symbol); 51 | ``` 52 | 53 | 调用 `client.run` 方法并传入一个回调函数以启动 client。回调函数接受一个 WebSocket 应答枚举类型作为其唯一参数。每当收到一条 WebSocket 应答消息时,该回调函数都会被调用: 54 | 55 | ```rust 56 | let callback = |res: SpotPublicResponse| match res { 57 | SpotPublicResponse::Orderbook(res) => println!("Orderbook: {:?}", res), 58 | SpotPublicResponse::Trade(res) => println!("Trade: {:?}", res), 59 | SpotPublicResponse::Ticker(res) => println!("Ticker: {:?}", res), 60 | SpotPublicResponse::Kline(res) => println!("Kline: {:?}", res), 61 | SpotPublicResponse::LtTicker(res) => println!("LtTicker: {:?}", res), 62 | SpotPublicResponse::LtNav(res) => println!("LtNav: {:?}", res), 63 | SpotPublicResponse::Op(res) => println!("Op: {:?}", res), 64 | }; 65 | 66 | match client.run(callback) { 67 | Ok(_) => {} 68 | Err(e) => println!("{}", e), 69 | } 70 | ``` 71 | 72 | 以上是一个简单打印接收到的 WebSocket 应答消息的例子。[examples](https://github.com/yufuquant/rust-bybit/tree/main/examples) 中还有一些更为实际的例子可供参考,例如通过订阅 [Orderbook](https://bybit-exchange.github.io/docs/zh-TW/v5/websocket/public/orderbook) 维护一个本地订单薄。你可以运行 `cargo run --example local_orderbook` 启动此示例程序,程序启动后将在终端实时显示 ETHUSDT 10 档订单薄行情。 73 | 74 | ## 捐赠 75 | 76 | 您可以向下面的钱包地址进行捐赠以支持此项目的长远发展。 77 | 78 | | 网络 | 钱包地址 | 79 | | ----------------------- | ------------------------------------------ | 80 | | Ethereum (ERC20) | 0x2ef22ed84D6b57496dbb95257C4eb8F02cE9b7A6 | 81 | | BNB Smart Chain (BEP20) | 0x869F8F9A78a18818F93061A02B233507b5F64151 | 82 | | Tron (TRC20) | TPvqJYHFQ7iqEgtEcYrSLTjpGsAq41dhFt | 83 | | Bitcoin | 3C6o4ADGFXyuf6TUXKL6YyMyRfhek6zxzx | 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rust-bybit 2 | 3 | [![Build Status]](https://github.com/yufuquant/rust-bybit/actions/workflows/ci.yaml) 4 | 5 | [build status]: https://github.com/yufuquant/rust-bybit/actions/workflows/ci.yaml/badge.svg?branch=main 6 | 7 | English | [简体中文](README-zh_CN.md) 8 | 9 | Unofficial Rust API connector for Bybit's WebSockets V5 APIs. 10 | 11 | ## Disclaimer 12 | 13 | This is an **unofficial** Rust API connector for Bybit's APIs and the user assumes all responsibility and risk for the use of this project. 14 | 15 | ## Installation 16 | 17 | Add this to Cargo.toml 18 | 19 | ```toml 20 | [dependencies] 21 | rust-bybit = "0.2" 22 | ``` 23 | 24 | ## Basic Usage 25 | 26 | Create a WebSocket client for specific channel: 27 | 28 | ```rust 29 | use bybit::ws::response::SpotPublicResponse; 30 | use bybit::ws::spot; 31 | use bybit::KlineInterval; 32 | use bybit::WebSocketApiClient; 33 | 34 | let mut client = WebSocketApiClient::spot().build(); 35 | ``` 36 | 37 | Subscribe to topics you are interested in. The following code will subscribe to all topics with symbol=ETHUSDT, or symbol=BTC3SUSDT for leveraged token (for all available topics, please check [Bybit V5 API](https://bybit-exchange.github.io/docs/v5/intro)). Note that the subscriptions will not be sent until `client.run` is called: 38 | 39 | ```rust 40 | let symbol = "ETHUSDT"; 41 | let lt_symbol = "BTC3SUSDT"; 42 | 43 | client.subscribe_orderbook(symbol, spot::OrderbookDepth::Level1); 44 | client.subscribe_orderbook(symbol, spot::OrderbookDepth::Level50); 45 | client.subscribe_trade(symbol); 46 | client.subscribe_ticker(symbol); 47 | client.subscribe_kline(symbol, KlineInterval::Min1); 48 | client.subscribe_lt_kline(lt_symbol, KlineInterval::Min5); 49 | client.subscribe_lt_ticker(lt_symbol); 50 | client.subscribe_lt_nav(lt_symbol); 51 | ``` 52 | 53 | Pass a callback function to `client.run` to start the client. The callback must accept exactly one parameter: the `Enum` which variants are WebSocket responses. The callback function will be called whenever a WebSocket response is received: 54 | 55 | ```rust 56 | let callback = |res: SpotPublicResponse| match res { 57 | SpotPublicResponse::Orderbook(res) => println!("Orderbook: {:?}", res), 58 | SpotPublicResponse::Trade(res) => println!("Trade: {:?}", res), 59 | SpotPublicResponse::Ticker(res) => println!("Ticker: {:?}", res), 60 | SpotPublicResponse::Kline(res) => println!("Kline: {:?}", res), 61 | SpotPublicResponse::LtTicker(res) => println!("LtTicker: {:?}", res), 62 | SpotPublicResponse::LtNav(res) => println!("LtNav: {:?}", res), 63 | SpotPublicResponse::Op(res) => println!("Op: {:?}", res), 64 | }; 65 | 66 | match client.run(callback) { 67 | Ok(_) => {} 68 | Err(e) => println!("{}", e), 69 | } 70 | ``` 71 | 72 | This is a simple example that just print the received WebSocket responses. There are some more complex [examples](https://github.com/yufuquant/rust-bybit/tree/main/examples) for real usage demonstration, such as maintaining a local order book. You can run `cargo run --example local_orderbook` to see how it works. 73 | 74 | ## Donate 75 | 76 | You can donate to following cryptocurrency wallet addresses to help this project going further. 77 | 78 | | Network | Address | 79 | | ----------------------- | ------------------------------------------ | 80 | | Ethereum (ERC20) | 0x2ef22ed84D6b57496dbb95257C4eb8F02cE9b7A6 | 81 | | BNB Smart Chain (BEP20) | 0x869F8F9A78a18818F93061A02B233507b5F64151 | 82 | | Tron (TRC20) | TPvqJYHFQ7iqEgtEcYrSLTjpGsAq41dhFt | 83 | | Bitcoin | 3C6o4ADGFXyuf6TUXKL6YyMyRfhek6zxzx | 84 | -------------------------------------------------------------------------------- /examples/future_inverse_ws_api_client.rs: -------------------------------------------------------------------------------- 1 | use bybit::ws::future; 2 | use bybit::ws::response::FuturePublicResponse; 3 | use bybit::KlineInterval; 4 | use bybit::WebSocketApiClient; 5 | 6 | fn main() { 7 | env_logger::init(); 8 | 9 | let mut client = WebSocketApiClient::future_inverse().build(); 10 | 11 | let symbol = "ETHUSD"; 12 | 13 | client.subscribe_orderbook(symbol, future::OrderbookDepth::Level1); 14 | client.subscribe_orderbook(symbol, future::OrderbookDepth::Level50); 15 | client.subscribe_trade(symbol); 16 | client.subscribe_ticker(symbol); 17 | client.subscribe_kline(symbol, KlineInterval::Min1); 18 | client.subscribe_liquidation(symbol); 19 | 20 | if let Err(e) = client.run(|res| match res { 21 | FuturePublicResponse::Orderbook(res) => println!("Orderbook: {:?}", res), 22 | FuturePublicResponse::Trade(res) => println!("Trade: {:?}", res), 23 | FuturePublicResponse::Ticker(res) => println!("Ticker: {:?}", res), 24 | FuturePublicResponse::Kline(res) => println!("Kline: {:?}", res), 25 | FuturePublicResponse::Liquidation(res) => println!("Liquidation: {:?}", res), 26 | FuturePublicResponse::Op(res) => println!("Op: {:?}", res), 27 | }) { 28 | eprintln!("Error: {e}") 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /examples/future_linear_ws_api_client.rs: -------------------------------------------------------------------------------- 1 | use bybit::ws::future; 2 | use bybit::ws::response::FuturePublicResponse; 3 | use bybit::KlineInterval; 4 | use bybit::WebSocketApiClient; 5 | 6 | fn main() { 7 | env_logger::init(); 8 | 9 | let mut client = WebSocketApiClient::future_linear().build(); 10 | 11 | let symbol = "ETHUSDT"; 12 | 13 | client.subscribe_orderbook(symbol, future::OrderbookDepth::Level1); 14 | client.subscribe_orderbook(symbol, future::OrderbookDepth::Level50); 15 | client.subscribe_trade(symbol); 16 | client.subscribe_ticker(symbol); 17 | client.subscribe_kline(symbol, KlineInterval::Min1); 18 | client.subscribe_liquidation(symbol); 19 | 20 | if let Err(e) = client.run(|res| match res { 21 | FuturePublicResponse::Orderbook(res) => println!("Orderbook: {:?}", res), 22 | FuturePublicResponse::Trade(res) => println!("Trade: {:?}", res), 23 | FuturePublicResponse::Ticker(res) => println!("Ticker: {:?}", res), 24 | FuturePublicResponse::Kline(res) => println!("Kline: {:?}", res), 25 | FuturePublicResponse::Liquidation(res) => println!("Liquidation: {:?}", res), 26 | FuturePublicResponse::Op(res) => println!("Op: {:?}", res), 27 | }) { 28 | eprintln!("Error: {e}") 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /examples/local_orderbook.rs: -------------------------------------------------------------------------------- 1 | use bybit::ws::response::{OrderbookItem, SpotPublicResponse}; 2 | use bybit::ws::spot; 3 | use bybit::WebSocketApiClient; 4 | use std::io::{self, Write}; 5 | 6 | struct OwnedOrderBookItem(String, String); 7 | 8 | impl<'a> From<&OrderbookItem<'a>> for OwnedOrderBookItem { 9 | fn from(value: &OrderbookItem) -> Self { 10 | OwnedOrderBookItem(value.0.to_owned(), value.1.to_owned()) 11 | } 12 | } 13 | 14 | fn main() { 15 | let mut client = WebSocketApiClient::spot().build(); 16 | 17 | let symbol = "ETHUSDT"; 18 | 19 | client.subscribe_trade(symbol); 20 | client.subscribe_orderbook(symbol, spot::OrderbookDepth::Level50); 21 | 22 | let stdout = io::stdout(); 23 | let mut handle = io::BufWriter::new(stdout); 24 | 25 | let mut latest_price: String = String::new(); 26 | let mut direction = "△"; 27 | let mut asks: Vec = Vec::new(); 28 | let mut bids: Vec = Vec::new(); 29 | 30 | let callback = |res: SpotPublicResponse| { 31 | match res { 32 | SpotPublicResponse::Trade(res) => { 33 | let price = res.data[0].p.to_owned(); 34 | if price < latest_price { 35 | direction = "▽"; 36 | } else if price > latest_price { 37 | direction = "△"; 38 | } 39 | latest_price = price 40 | } 41 | SpotPublicResponse::Orderbook(res) => { 42 | // > Once you have subscribed successfully, you will receive a snapshot. 43 | // > If you receive a new snapshot message, you will have to reset your local orderbook. 44 | if res.type_ == "snapshot" { 45 | asks = res.data.a.iter().map(|item| item.into()).collect(); 46 | bids = res.data.b.iter().map(|item| item.into()).collect(); 47 | return; 48 | } 49 | 50 | // Receive a delta message, update the orderbook. 51 | // Note that asks and bids of a delta message **do not guarantee** to be ordered. 52 | 53 | // process asks 54 | let a = &res.data.a; 55 | let mut i: usize = 0; 56 | 57 | while i < a.len() { 58 | let OrderbookItem(price, qty) = a[i]; 59 | 60 | let mut j: usize = 0; 61 | while j < asks.len() { 62 | let item = &mut asks[j]; 63 | let item_price: &str = &item.0; 64 | 65 | if price < item_price { 66 | asks.insert(j, OwnedOrderBookItem(price.to_owned(), qty.to_owned())); 67 | break; 68 | } 69 | 70 | if price == item_price { 71 | if qty != "0" { 72 | item.1 = qty.to_owned(); 73 | } else { 74 | asks.remove(j); 75 | } 76 | break; 77 | } 78 | 79 | j += 1; 80 | } 81 | 82 | if j == asks.len() { 83 | asks.push(OwnedOrderBookItem(price.to_owned(), qty.to_owned())) 84 | } 85 | 86 | i += 1; 87 | } 88 | 89 | // process bids 90 | let b = &res.data.b; 91 | let mut i: usize = 0; 92 | 93 | while i < b.len() { 94 | let OrderbookItem(price, qty) = b[i]; 95 | 96 | let mut j: usize = 0; 97 | while j < bids.len() { 98 | let item = &mut bids[j]; 99 | let item_price: &str = &item.0; 100 | if price > item_price { 101 | bids.insert(j, OwnedOrderBookItem(price.to_owned(), qty.to_owned())); 102 | break; 103 | } 104 | 105 | if price == item_price { 106 | if qty != "0" { 107 | item.1 = qty.to_owned(); 108 | } else { 109 | bids.remove(j); 110 | } 111 | break; 112 | } 113 | 114 | j += 1; 115 | } 116 | 117 | if j == bids.len() { 118 | bids.push(OwnedOrderBookItem(price.to_owned(), qty.to_owned())); 119 | } 120 | 121 | i += 1; 122 | } 123 | } 124 | _ => {} 125 | } 126 | 127 | write!(handle, "\x1B[2J\x1B[1;1H").unwrap(); 128 | write!(handle, "ETH/USDT\n\n").unwrap(); 129 | write!(handle, "{:<20} {:<20}\n", "Price(USDT)", "Quantity(ETH)").unwrap(); 130 | let mut asks10 = asks.iter().take(10).collect::>().clone(); 131 | asks10.reverse(); 132 | asks10.iter().for_each(|item| { 133 | write!(handle, "{:<20} {:<20}\n", item.0, item.1).unwrap(); 134 | }); 135 | write!(handle, "\n{} {}\n\n", direction, latest_price).unwrap(); 136 | bids.iter().take(10).for_each(|item| { 137 | write!(handle, "{:<20} {:<20}\n", item.0, item.1).unwrap(); 138 | }); 139 | handle.flush().unwrap(); 140 | }; 141 | 142 | match client.run(callback) { 143 | Ok(_) => {} 144 | Err(e) => eprintln!("{}", e), 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /examples/option_ws_api_client.rs: -------------------------------------------------------------------------------- 1 | use bybit::ws::option; 2 | use bybit::ws::response::OptionPublicResponse; 3 | use bybit::WebSocketApiClient; 4 | 5 | fn main() { 6 | env_logger::init(); 7 | 8 | let mut client = WebSocketApiClient::option().testnet().build(); 9 | 10 | let symbol = "BTC-10MAR23-16000-C"; 11 | let base_coin = "BTC"; 12 | 13 | client.subscribe_orderbook(symbol, option::OrderbookDepth::Level25); 14 | client.subscribe_orderbook(symbol, option::OrderbookDepth::Level100); 15 | client.subscribe_trade(base_coin); 16 | client.subscribe_ticker(symbol); 17 | 18 | let callback = |res: OptionPublicResponse| match res { 19 | OptionPublicResponse::Orderbook(res) => println!("Orderbook: {:?}", res), 20 | OptionPublicResponse::Trade(res) => println!("Trade: {:?}", res), 21 | OptionPublicResponse::Ticker(res) => println!("Ticker: {:?}", res), 22 | OptionPublicResponse::Pong(res) => println!("Pong: {:?}", res), 23 | OptionPublicResponse::Subscription(res) => println!("Subscription: {:?}", res), 24 | }; 25 | 26 | if let Err(e) = client.run(callback) { 27 | eprintln!("Error: {e}") 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/private_ws_api_client.rs: -------------------------------------------------------------------------------- 1 | use bybit::ws::response::PrivateResponse; 2 | use bybit::WebSocketApiClient; 3 | use std::env; 4 | 5 | fn main() { 6 | env_logger::init(); 7 | 8 | let api_key: String = env::var("BYBIT_API_KEY").unwrap(); 9 | let secret: String = env::var("BYBIT_SECRET").unwrap(); 10 | 11 | let mut client = WebSocketApiClient::private() 12 | .testnet() 13 | .build_with_credentials(api_key, secret); 14 | 15 | client.subscribe_position(); 16 | client.subscribe_execution(); 17 | client.subscribe_order(); 18 | client.subscribe_wallet(); 19 | client.subscribe_greek(); 20 | 21 | if let Err(e) = client.run(|res| match res { 22 | PrivateResponse::Position(res) => println!("Position: {:?}", res), 23 | PrivateResponse::Execution(res) => println!("Execution: {:?}", res), 24 | PrivateResponse::Order(res) => println!("Order: {:?}", res), 25 | PrivateResponse::Wallet(res) => println!("Wallet: {:?}", res), 26 | PrivateResponse::Greek(res) => println!("Greek: {:?}", res), 27 | PrivateResponse::Pong(res) => println!("Pong: {:?}", res), 28 | PrivateResponse::Op(res) => println!("Op: {:?}", res), 29 | }) { 30 | eprintln!("Error: {e}") 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/spot_ws_api_client.rs: -------------------------------------------------------------------------------- 1 | use bybit::ws::response::SpotPublicResponse; 2 | use bybit::ws::spot; 3 | use bybit::KlineInterval; 4 | use bybit::WebSocketApiClient; 5 | 6 | fn main() { 7 | env_logger::init(); 8 | 9 | let mut client = WebSocketApiClient::spot().build(); 10 | 11 | let symbol = "ETHUSDT"; 12 | let lt_symbol = "BTC3SUSDT"; 13 | 14 | client.subscribe_orderbook(symbol, spot::OrderbookDepth::Level1); 15 | client.subscribe_orderbook(symbol, spot::OrderbookDepth::Level50); 16 | client.subscribe_trade(symbol); 17 | client.subscribe_ticker(symbol); 18 | client.subscribe_kline(symbol, KlineInterval::Min1); 19 | client.subscribe_lt_kline(lt_symbol, KlineInterval::Min5); 20 | client.subscribe_lt_ticker(lt_symbol); 21 | client.subscribe_lt_nav(lt_symbol); 22 | 23 | let callback = |res: SpotPublicResponse| match res { 24 | SpotPublicResponse::Orderbook(res) => println!("Orderbook: {:?}", res), 25 | SpotPublicResponse::Trade(res) => println!("Trade: {:?}", res), 26 | SpotPublicResponse::Ticker(res) => println!("Ticker: {:?}", res), 27 | SpotPublicResponse::Kline(res) => println!("Kline: {:?}", res), 28 | SpotPublicResponse::LtTicker(res) => println!("LtTicker: {:?}", res), 29 | SpotPublicResponse::LtNav(res) => println!("LtNav: {:?}", res), 30 | SpotPublicResponse::Op(res) => println!("Op: {:?}", res), 31 | }; 32 | 33 | if let Err(e) = client.run(callback) { 34 | eprintln!("Error: {e}") 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use serde_json::error::Error as SerdeError; 2 | use std::result; 3 | use thiserror::Error; 4 | use tungstenite::error::Error as TungsteniteError; 5 | 6 | pub type Result = result::Result; 7 | 8 | #[derive(Error, Debug)] 9 | pub enum BybitError { 10 | #[error("Serde error: {0}")] 11 | SerdeError(#[from] SerdeError), 12 | 13 | #[error("Tungstenite error: {0}")] 14 | TungsteniteError(#[from] TungsteniteError), 15 | } 16 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod error; 2 | pub mod util; 3 | pub mod ws; 4 | 5 | pub use ws::WebSocketApiClient; 6 | 7 | pub enum FutureRole { 8 | Linear, 9 | Inverse, 10 | } 11 | 12 | pub enum KlineInterval { 13 | Min1, 14 | Min3, 15 | Min5, 16 | Min15, 17 | Min30, 18 | Min60, 19 | Min120, 20 | Min240, 21 | Min360, 22 | Min720, 23 | Day, 24 | Week, 25 | Month, 26 | } 27 | 28 | impl From for &str { 29 | fn from(value: KlineInterval) -> Self { 30 | use KlineInterval::*; 31 | match value { 32 | Min1 => "1", 33 | Min3 => "3", 34 | Min5 => "5", 35 | Min15 => "15", 36 | Min30 => "30", 37 | Min60 => "60", 38 | Min120 => "120", 39 | Min240 => "240", 40 | Min360 => "360", 41 | Min720 => "720", 42 | Day => "D", 43 | Week => "W", 44 | Month => "M", 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use hex; 2 | use ring::hmac; 3 | use std::time::SystemTime; 4 | 5 | pub fn millis() -> u128 { 6 | SystemTime::now() 7 | .duration_since(SystemTime::UNIX_EPOCH) 8 | .unwrap() 9 | .as_millis() 10 | } 11 | 12 | pub fn sign(secret: &str, msg: &str) -> String { 13 | let key = hmac::Key::new(hmac::HMAC_SHA256, secret.as_bytes()); 14 | let tag = hmac::sign(&key, msg.as_bytes()); 15 | hex::encode(tag.as_ref()) 16 | } 17 | 18 | #[cfg(test)] 19 | mod tests { 20 | use super::*; 21 | 22 | #[test] 23 | fn test_millseconds() { 24 | assert!(millis() > 0); 25 | } 26 | 27 | #[test] 28 | fn test_sign() { 29 | assert_eq!( 30 | sign("secret", "message"), 31 | String::from("8b5f48702995c1598c573db1e21866a9b825d4a794d169d7060a03605796360b") 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/ws/callback.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | pub trait Arg { 4 | type ValueType<'a>: Deserialize<'a> 5 | where 6 | Self: 'a; 7 | } 8 | 9 | pub trait Callback: for<'any> FnMut(A::ValueType<'any>) {} 10 | impl FnMut(A::ValueType<'any>)> Callback for F {} 11 | -------------------------------------------------------------------------------- /src/ws/future.rs: -------------------------------------------------------------------------------- 1 | use super::callback::Callback; 2 | use super::response::FuturePublicResponseArg; 3 | use super::run; 4 | use super::Subscriber; 5 | use crate::error::Result; 6 | use crate::{FutureRole, KlineInterval}; 7 | 8 | const MAINNET_LINEAR: &str = "wss://stream.bybit.com/v5/public/linear"; 9 | const MAINNET_INVERSE: &str = "wss://stream.bybit.com/v5/public/inverse"; 10 | const TESTNET_LINEAR: &str = "wss://stream-testnet.bybit.com/v5/public/linear"; 11 | const TESTNET_INVERSE: &str = "wss://stream-testnet.bybit.com/v5/public/inverse"; 12 | 13 | pub enum OrderbookDepth { 14 | Level1, 15 | Level50, 16 | Level200, 17 | Level500, 18 | } 19 | 20 | impl From for u16 { 21 | fn from(value: OrderbookDepth) -> Self { 22 | use OrderbookDepth::*; 23 | match value { 24 | Level1 => 1, 25 | Level50 => 50, 26 | Level200 => 200, 27 | Level500 => 500, 28 | } 29 | } 30 | } 31 | 32 | pub struct FutureWebsocketApiClient { 33 | uri: String, 34 | subscriber: Subscriber, 35 | } 36 | 37 | impl FutureWebsocketApiClient { 38 | pub fn subscribe_orderbook>(&mut self, symbol: S, depth: OrderbookDepth) { 39 | self.subscriber.sub_orderbook(symbol.as_ref(), depth.into()); 40 | } 41 | 42 | pub fn subscribe_trade>(&mut self, symbol: S) { 43 | self.subscriber.sub_trade(symbol.as_ref()); 44 | } 45 | 46 | pub fn subscribe_ticker>(&mut self, symbol: S) { 47 | self.subscriber.sub_ticker(symbol.as_ref()); 48 | } 49 | 50 | pub fn subscribe_kline>(&mut self, symbol: S, interval: KlineInterval) { 51 | self.subscriber.sub_kline(symbol.as_ref(), interval.into()); 52 | } 53 | 54 | pub fn subscribe_liquidation>(&mut self, symbol: S) { 55 | self.subscriber.sub_liquidation(symbol.as_ref()); 56 | } 57 | 58 | pub fn run>(&self, callback: C) -> Result<()> { 59 | run(&self.uri, self.subscriber.topics(), None, callback) 60 | } 61 | } 62 | 63 | pub struct FutureWebSocketApiClientBuilder { 64 | uri: String, 65 | role: FutureRole, 66 | } 67 | 68 | impl FutureWebSocketApiClientBuilder { 69 | /// Create a new `FutureWebSocketApiClientBuilder`. Channel URI is set to the mainnet. 70 | pub fn new(role: FutureRole) -> Self { 71 | let uri = match role { 72 | FutureRole::Linear => MAINNET_LINEAR.to_string(), 73 | FutureRole::Inverse => MAINNET_INVERSE.to_string(), 74 | }; 75 | Self { uri, role } 76 | } 77 | 78 | /// Change channel URI to the testnet. 79 | pub fn testnet(mut self) -> Self { 80 | self.uri = match self.role { 81 | FutureRole::Linear => TESTNET_LINEAR.to_string(), 82 | FutureRole::Inverse => TESTNET_INVERSE.to_string(), 83 | }; 84 | self 85 | } 86 | 87 | /// Set channel URI to the URI specified. 88 | /// 89 | /// Note URI should **match** with api client kind. 90 | /// Do not set a spot channel URI to a future api client. 91 | pub fn uri>(mut self, uri: S) -> Self { 92 | self.uri = uri.as_ref().to_owned(); 93 | self 94 | } 95 | 96 | /// Build a future websocket api client. 97 | pub fn build(self) -> FutureWebsocketApiClient { 98 | FutureWebsocketApiClient { 99 | uri: self.uri, 100 | subscriber: Subscriber::new(), 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/ws/mod.rs: -------------------------------------------------------------------------------- 1 | mod callback; 2 | pub mod future; 3 | pub mod option; 4 | pub mod private; 5 | pub mod response; 6 | pub mod spot; 7 | 8 | use callback::Arg; 9 | use callback::Callback; 10 | use log::*; 11 | use serde::Serialize; 12 | use std::net::TcpStream; 13 | use std::sync::mpsc::Receiver; 14 | use std::{sync::mpsc, thread, time::Duration}; 15 | use tungstenite::{connect, stream::MaybeTlsStream, Message, WebSocket}; 16 | 17 | use crate::error::Result; 18 | use crate::util::millis; 19 | use crate::util::sign; 20 | use crate::FutureRole; 21 | 22 | use self::future::FutureWebSocketApiClientBuilder; 23 | use self::option::OptionWebSocketApiClientBuilder; 24 | use self::private::PrivateWebSocketApiClientBuilder; 25 | use self::spot::SpotWebSocketApiClientBuilder; 26 | 27 | /// A factory to create different kind of websocket api clients (spot / future / option / private). 28 | pub struct WebSocketApiClient; 29 | 30 | impl WebSocketApiClient { 31 | /// Get a builder for building spot websocket api client. 32 | pub fn spot() -> SpotWebSocketApiClientBuilder { 33 | SpotWebSocketApiClientBuilder::new() 34 | } 35 | 36 | /// Get a builder for building inverse future websocket api client. 37 | pub fn future_inverse() -> FutureWebSocketApiClientBuilder { 38 | FutureWebSocketApiClientBuilder::new(FutureRole::Inverse) 39 | } 40 | 41 | /// Get a builder for building linear future websocket api client. 42 | pub fn future_linear() -> FutureWebSocketApiClientBuilder { 43 | FutureWebSocketApiClientBuilder::new(FutureRole::Linear) 44 | } 45 | 46 | /// Get a builder for building option websocket api client. 47 | pub fn option() -> OptionWebSocketApiClientBuilder { 48 | OptionWebSocketApiClientBuilder::new() 49 | } 50 | 51 | /// Get a builder for building private websocket api client. 52 | pub fn private() -> PrivateWebSocketApiClientBuilder { 53 | PrivateWebSocketApiClientBuilder::new() 54 | } 55 | } 56 | 57 | struct Subscriber { 58 | topics: Vec, 59 | } 60 | 61 | impl Subscriber { 62 | fn new() -> Self { 63 | Self { topics: Vec::new() } 64 | } 65 | 66 | fn topics(&self) -> &Vec { 67 | &self.topics 68 | } 69 | 70 | fn sub_orderbook(&mut self, symbol: &str, depth: u16) { 71 | self.sub(format!("orderbook.{depth}.{symbol}")); 72 | } 73 | 74 | fn sub_ticker(&mut self, symbol: &str) { 75 | self.sub(format!("tickers.{symbol}")); 76 | } 77 | 78 | fn sub_trade(&mut self, symbol: &str) { 79 | self.sub(format!("publicTrade.{symbol}")); 80 | } 81 | 82 | fn sub_kline(&mut self, symbol: &str, interval: &str) { 83 | self.sub(format!("kline.{interval}.{symbol}")); 84 | } 85 | 86 | fn sub_liquidation(&mut self, symbol: &str) { 87 | self.sub(format!("liquidation.{symbol}")); 88 | } 89 | 90 | fn sub_lt_kline(&mut self, symbol: &str, interval: &str) { 91 | self.sub(format!("kline_lt.{interval}.{symbol}")); 92 | } 93 | 94 | fn sub_lt_ticker(&mut self, symbol: &str) { 95 | self.sub(format!("tickers_lt.{symbol}")); 96 | } 97 | 98 | fn sub_lt_nav(&mut self, symbol: &str) { 99 | self.sub(format!("lt.{symbol}")); 100 | } 101 | 102 | fn sub_position(&mut self) { 103 | self.sub("position".to_string()); 104 | } 105 | 106 | fn sub_execution(&mut self) { 107 | self.sub("execution".to_string()); 108 | } 109 | 110 | fn sub_order(&mut self) { 111 | self.sub("order".to_string()); 112 | } 113 | 114 | fn sub_wallet(&mut self) { 115 | self.sub("wallet".to_string()); 116 | } 117 | 118 | fn sub_greek(&mut self) { 119 | self.sub("greeks".to_string()); 120 | } 121 | 122 | fn sub(&mut self, topic: String) { 123 | self.topics.push(topic); 124 | } 125 | } 126 | 127 | #[derive(Serialize)] 128 | struct Op<'a> { 129 | op: &'a str, 130 | args: Vec, 131 | } 132 | 133 | struct Credentials { 134 | api_key: String, 135 | secret: String, 136 | } 137 | 138 | fn run( 139 | uri: &str, 140 | topics: &Vec, 141 | credentials: Option<&Credentials>, 142 | mut callback: C, 143 | ) -> Result<()> 144 | where 145 | A: Arg, 146 | C: Callback, 147 | { 148 | let (mut ws, _) = connect(uri)?; 149 | 150 | // Set read timeout to the underlying TCP stream. 151 | // 152 | // Read and write are both in the main thread loop. A blocking read call 153 | // will starve writing that causes ping op message can't be sent on time. 154 | // Read timeout mitigate this situation. 155 | set_read_timeout(&ws); 156 | 157 | // Authenticate 158 | if let Some(credentials) = credentials { 159 | let req = auth_req(credentials); 160 | ws.write_message(Message::Text(req))?; 161 | } 162 | 163 | // Subscribe 164 | ws.write_message(Message::Text(subscription(topics)))?; 165 | 166 | let rx = ping(); 167 | loop { 168 | // Ping 169 | if let Ok(ping) = rx.try_recv() { 170 | ws.write_message(Message::Text(ping.into()))? 171 | } 172 | 173 | match ws.read_message() { 174 | Ok(msg) => match msg { 175 | Message::Text(content) => { 176 | debug!("Received: {}", content); 177 | match serde_json::from_str(&content) { 178 | Ok(res) => callback(res), 179 | Err(e) => error!("Error: {}", e), 180 | } 181 | } 182 | _ => {} 183 | }, 184 | Err(e) => match e { 185 | tungstenite::Error::Io(ref ee) => { 186 | if ee.kind() != std::io::ErrorKind::WouldBlock 187 | && ee.kind() != std::io::ErrorKind::TimedOut 188 | { 189 | Err(e)? 190 | } 191 | } 192 | _ => Err(e)?, 193 | }, 194 | } 195 | } 196 | } 197 | 198 | fn set_read_timeout(ws: &WebSocket>) { 199 | match ws.get_ref() { 200 | MaybeTlsStream::Plain(s) => { 201 | s.set_read_timeout(Some(Duration::from_secs(10))).unwrap(); 202 | } 203 | MaybeTlsStream::NativeTls(t) => { 204 | t.get_ref() 205 | .set_read_timeout(Some(Duration::from_secs(10))) 206 | .unwrap(); 207 | } 208 | _ => unreachable!(), 209 | }; 210 | } 211 | 212 | fn auth_req(credentials: &Credentials) -> String { 213 | let expires = millis() + 10000; 214 | let val = format!("GET/realtime{}", expires); 215 | let signature = sign(&credentials.secret, &val); 216 | let auth_req = Op { 217 | op: "auth", 218 | args: vec![credentials.api_key.clone(), expires.to_string(), signature], 219 | }; 220 | serde_json::to_string(&auth_req).unwrap() 221 | } 222 | 223 | fn subscription(topics: &Vec) -> String { 224 | let sub = Op { 225 | op: "subscribe", 226 | args: topics.clone(), 227 | }; 228 | serde_json::to_string(&sub).unwrap() 229 | } 230 | 231 | fn ping() -> Receiver<&'static str> { 232 | let (tx, rx) = mpsc::channel(); 233 | thread::spawn(move || loop { 234 | if let Err(_) = tx.send("{\"op\":\"ping\"}") { 235 | break; 236 | }; 237 | thread::sleep(Duration::from_secs(20)); 238 | }); 239 | rx 240 | } 241 | -------------------------------------------------------------------------------- /src/ws/option.rs: -------------------------------------------------------------------------------- 1 | use super::callback::Callback; 2 | use super::response::OptionPublicResponseArg; 3 | use super::run; 4 | use super::Subscriber; 5 | use crate::error::Result; 6 | 7 | const MAINNET_OPTION: &str = "wss://stream.bybit.com/v5/public/option"; 8 | const TESTNET_OPTION: &str = "wss://stream-testnet.bybit.com/v5/public/option"; 9 | 10 | pub enum OrderbookDepth { 11 | Level25, 12 | Level100, 13 | } 14 | 15 | impl From for u16 { 16 | fn from(value: OrderbookDepth) -> Self { 17 | use OrderbookDepth::*; 18 | match value { 19 | Level25 => 25, 20 | Level100 => 100, 21 | } 22 | } 23 | } 24 | 25 | pub struct OptionWebsocketApiClient { 26 | uri: String, 27 | subscriber: Subscriber, 28 | } 29 | 30 | impl OptionWebsocketApiClient { 31 | pub fn subscribe_orderbook>(&mut self, symbol: S, depth: OrderbookDepth) { 32 | self.subscriber.sub_orderbook(symbol.as_ref(), depth.into()); 33 | } 34 | 35 | /// Subscribe to recent trades. 36 | /// 37 | /// Note that option uses the base coin, e.g., BTC. 38 | pub fn subscribe_trade>(&mut self, base_coin: S) { 39 | self.subscriber.sub_trade(base_coin.as_ref()); 40 | } 41 | 42 | pub fn subscribe_ticker>(&mut self, symbol: S) { 43 | self.subscriber.sub_ticker(symbol.as_ref()); 44 | } 45 | 46 | pub fn run>(&self, callback: C) -> Result<()> { 47 | run(&self.uri, self.subscriber.topics(), None, callback) 48 | } 49 | } 50 | 51 | pub struct OptionWebSocketApiClientBuilder { 52 | uri: String, 53 | } 54 | 55 | impl OptionWebSocketApiClientBuilder { 56 | /// Create a new `OptionWebSocketApiClientBuilder`. Channel URI is set to the mainnet. 57 | pub fn new() -> Self { 58 | Self { 59 | uri: MAINNET_OPTION.to_string(), 60 | } 61 | } 62 | 63 | /// Change channel URI to the testnet. 64 | pub fn testnet(mut self) -> Self { 65 | self.uri = TESTNET_OPTION.to_string(); 66 | self 67 | } 68 | 69 | /// Set channel URI to the URI specified. 70 | /// 71 | /// Note URI should **match** with api client kind. 72 | /// Do not set a spot channel URI to a option api client. 73 | pub fn uri>(mut self, uri: S) -> Self { 74 | self.uri = uri.as_ref().to_owned(); 75 | self 76 | } 77 | 78 | /// Build a option websocket api client. 79 | pub fn build(self) -> OptionWebsocketApiClient { 80 | OptionWebsocketApiClient { 81 | uri: self.uri, 82 | subscriber: Subscriber::new(), 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/ws/private.rs: -------------------------------------------------------------------------------- 1 | use super::callback::Callback; 2 | use super::response::PrivateResponseArg; 3 | use super::Subscriber; 4 | use super::{run, Credentials}; 5 | use crate::error::Result; 6 | 7 | const MAINNET_PRIVATE: &str = "wss://stream.bybit.com/v5/private"; 8 | const TESTNET_PRIVATE: &str = "wss://stream-testnet.bybit.com/v5/private"; 9 | 10 | pub struct PrivateWebsocketApiClient { 11 | uri: String, 12 | subscriber: Subscriber, 13 | credentials: Credentials, 14 | } 15 | 16 | impl PrivateWebsocketApiClient { 17 | pub fn subscribe_position(&mut self) { 18 | self.subscriber.sub_position(); 19 | } 20 | 21 | pub fn subscribe_order(&mut self) { 22 | self.subscriber.sub_order(); 23 | } 24 | 25 | pub fn subscribe_wallet(&mut self) { 26 | self.subscriber.sub_wallet(); 27 | } 28 | 29 | pub fn subscribe_execution(&mut self) { 30 | self.subscriber.sub_execution(); 31 | } 32 | 33 | pub fn subscribe_greek(&mut self) { 34 | self.subscriber.sub_greek(); 35 | } 36 | 37 | pub fn run>(&self, callback: C) -> Result<()> { 38 | run( 39 | &self.uri, 40 | self.subscriber.topics(), 41 | Some(&self.credentials), 42 | callback, 43 | ) 44 | } 45 | } 46 | 47 | pub struct PrivateWebSocketApiClientBuilder { 48 | uri: String, 49 | } 50 | 51 | impl PrivateWebSocketApiClientBuilder { 52 | /// Create a new `PrivateWebSocketApiClientBuilder`. Channel URI is set to the mainnet. 53 | pub fn new() -> Self { 54 | Self { 55 | uri: MAINNET_PRIVATE.to_string(), 56 | } 57 | } 58 | 59 | /// Change channel URI to the testnet. 60 | pub fn testnet(mut self) -> Self { 61 | self.uri = TESTNET_PRIVATE.to_string(); 62 | self 63 | } 64 | 65 | /// Set channel URI to the URI specified. 66 | /// 67 | /// Note URI should **match** with api client kind. 68 | /// Do not set a spot channel URI to a private api client. 69 | pub fn uri>(mut self, uri: S) -> Self { 70 | self.uri = uri.as_ref().to_owned(); 71 | self 72 | } 73 | 74 | /// Build a private websocket api client with api key and secret key. 75 | pub fn build_with_credentials>( 76 | self, 77 | api_key: S, 78 | secret: S, 79 | ) -> PrivateWebsocketApiClient { 80 | PrivateWebsocketApiClient { 81 | uri: self.uri, 82 | subscriber: Subscriber::new(), 83 | credentials: Credentials { 84 | api_key: api_key.as_ref().to_owned(), 85 | secret: secret.as_ref().to_owned(), 86 | }, 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/ws/response.rs: -------------------------------------------------------------------------------- 1 | use super::callback::Arg; 2 | use serde::Deserialize; 3 | 4 | /// The pong/subscription response. 5 | #[derive(Deserialize, Debug)] 6 | pub struct OpResponse<'a> { 7 | pub success: bool, 8 | pub ret_msg: &'a str, 9 | pub conn_id: &'a str, 10 | pub req_id: Option<&'a str>, 11 | pub op: &'a str, 12 | } 13 | 14 | /// The option pong response of public channels. 15 | #[derive(Deserialize, Debug)] 16 | pub struct OptionPongResponse<'a> { 17 | pub args: [&'a str; 1], 18 | pub op: &'a str, 19 | } 20 | 21 | /// The data in option subscription response. 22 | #[derive(Deserialize, Debug)] 23 | #[serde(rename_all = "camelCase")] 24 | pub struct OptionSubscriptionData<'a> { 25 | #[serde(borrow)] 26 | pub fail_topics: Vec<&'a str>, 27 | pub success_topics: Vec<&'a str>, 28 | } 29 | 30 | /// The option subscription response. 31 | #[derive(Deserialize, Debug)] 32 | pub struct OptionSubscriptionResponse<'a> { 33 | pub success: bool, 34 | pub conn_id: &'a str, 35 | pub data: OptionSubscriptionData<'a>, 36 | #[serde(alias = "type")] 37 | pub type_: &'a str, 38 | } 39 | 40 | /// The pong response of private channels. 41 | #[derive(Deserialize, Debug)] 42 | pub struct PrivatePongResponse<'a> { 43 | pub req_id: Option<&'a str>, 44 | pub op: &'a str, 45 | pub args: [&'a str; 1], 46 | pub conn_id: &'a str, 47 | } 48 | 49 | /// The base response which contains common fields of public channels. 50 | #[derive(Deserialize, Debug)] 51 | pub struct BasePublicResponse<'a, Data> { 52 | /// Topic name. 53 | pub topic: &'a str, 54 | /// Data type. `snapshot`, `delta`. 55 | #[serde(alias = "type")] 56 | pub type_: &'a str, 57 | /// The timestamp (ms) that the system generates the data. 58 | pub ts: u64, 59 | /// The data vary on the topic. 60 | pub data: Data, 61 | } 62 | 63 | /// The base ticker response which contains common fields. 64 | #[derive(Deserialize, Debug)] 65 | pub struct BaseTickerPublicResponse<'a, Data> { 66 | /// Topic name. 67 | pub topic: &'a str, 68 | /// Data type. `snapshot`, `delta`. 69 | #[serde(alias = "type")] 70 | pub type_: &'a str, 71 | /// Cross sequence. 72 | pub cs: u64, 73 | /// The timestamp (ms) that the system generates the data. 74 | pub ts: u64, 75 | /// The spot/future ticker data. 76 | pub data: Data, 77 | } 78 | 79 | #[derive(Deserialize, Debug)] 80 | pub struct BaseOptionPublicResponse<'a, Data> { 81 | /// message ID 82 | pub id: &'a str, 83 | /// Topic name. 84 | pub topic: &'a str, 85 | #[serde(alias = "type")] 86 | /// Data type. `snapshot`. 87 | pub type_: &'a str, 88 | /// The timestamp (ms) that the system generates the data. 89 | pub ts: u64, 90 | /// The data vary on the topic. 91 | pub data: Data, 92 | } 93 | 94 | /// The base response which contains common fields of private channels. 95 | #[derive(Deserialize, Debug)] 96 | #[serde(rename_all = "camelCase")] 97 | pub struct BasePrivateResponse<'a, Data> { 98 | /// Message ID. 99 | pub id: &'a str, 100 | /// Topic name. 101 | pub topic: &'a str, 102 | /// Data created timestamp (ms). 103 | pub creation_time: u64, 104 | /// The data vary on the topic. 105 | pub data: Data, 106 | } 107 | 108 | /// The (price, size) pair of orderbook. 109 | #[derive(Deserialize, Debug)] 110 | pub struct OrderbookItem<'a>(pub &'a str, pub &'a str); 111 | 112 | /// The orderbook data. 113 | #[derive(Deserialize, Debug)] 114 | pub struct Orderbook<'a> { 115 | /// Symbol name. 116 | pub s: &'a str, 117 | /// Bids. For `snapshot` stream, the element is sorted by price in descending order. 118 | pub b: Vec>, 119 | /// Asks. For `snapshot` stream, the element is sorted by price in ascending order. 120 | pub a: Vec>, 121 | /// Update ID. Is a sequence. 122 | /// Occasionally, you'll receive "u"=1, which is a snapshot data due to the restart of the service. 123 | /// So please overwrite your local orderbook. 124 | pub u: u64, 125 | /// Cross sequence. Option does not have this field. 126 | pub seq: Option, 127 | } 128 | 129 | /// The trade data. 130 | #[allow(non_snake_case)] 131 | #[derive(Deserialize, Debug)] 132 | pub struct Trade<'a> { 133 | /// The timestamp (ms) that the order is filled. 134 | pub T: u64, 135 | /// Symbol name. 136 | pub s: &'a str, 137 | /// Side. `Buy`, `Sell`. 138 | pub S: &'a str, 139 | /// Trade size. 140 | pub v: &'a str, 141 | /// Trade price. 142 | pub p: &'a str, 143 | /// Direction of price change. Unique field for future. 144 | pub L: Option<&'a str>, 145 | /// Trade ID. 146 | pub i: &'a str, 147 | /// Whether it is a block trade order or not. 148 | pub BT: bool, 149 | } 150 | 151 | /// The spot ticker data. (`snapshot` only) 152 | #[derive(Deserialize, Debug)] 153 | #[serde(rename_all = "camelCase")] 154 | pub struct SpotTicker<'a> { 155 | /// Symbol name. 156 | pub symbol: &'a str, 157 | /// Last price. 158 | pub last_price: &'a str, 159 | /// The highest price in the last 24 hours. 160 | pub high_price_24h: &'a str, 161 | /// The lowest price in the last 24 hours. 162 | pub low_price_24h: &'a str, 163 | /// Percentage change of market price relative to 24h. 164 | pub prev_price_24h: &'a str, 165 | /// Volume for 24h. 166 | pub volume_24h: &'a str, 167 | /// Turnover for 24h. 168 | pub turnover_24h: &'a str, 169 | /// Percentage change of market price relative to 24h. 170 | pub price_24h_pcnt: &'a str, 171 | /// USD index price. It can be empty. 172 | pub usd_index_price: &'a str, 173 | } 174 | 175 | /// The option ticker data. (`snapshot` only) 176 | #[derive(Deserialize, Debug)] 177 | #[serde(rename_all = "camelCase")] 178 | pub struct OptionTicker<'a> { 179 | /// Symbol name. 180 | pub symbol: &'a str, 181 | /// Best bid price. 182 | pub bid_price: &'a str, 183 | /// Best bid size. 184 | pub bid_size: &'a str, 185 | /// Best bid iv. 186 | pub bid_iv: &'a str, 187 | /// Best ask price. 188 | pub ask_price: &'a str, 189 | /// Best ask size. 190 | pub ask_size: &'a str, 191 | /// Best ask iv. 192 | pub ask_iv: &'a str, 193 | /// Last price. 194 | pub last_price: &'a str, 195 | /// The highest price in the last 24 hours. 196 | pub high_price_24h: &'a str, 197 | /// The lowest price in the last 24 hours. 198 | pub low_price_24h: &'a str, 199 | /// Market price. 200 | pub mark_price: &'a str, 201 | /// Index price. 202 | pub index_price: &'a str, 203 | /// Mark price iv. 204 | pub mark_price_iv: &'a str, 205 | /// Underlying price. 206 | pub underlying_price: &'a str, 207 | /// Open interest size. 208 | pub open_interest: &'a str, 209 | /// Turnover for 24h. 210 | pub turnover_24h: &'a str, 211 | /// Volume for 24h. 212 | pub volume_24h: &'a str, 213 | /// Total volume. 214 | pub total_volume: &'a str, 215 | /// Total turnover. 216 | pub total_turnover: &'a str, 217 | /// Delta. 218 | pub delta: &'a str, 219 | /// Gamma. 220 | pub gamma: &'a str, 221 | /// Vega. 222 | pub vega: &'a str, 223 | /// Theta. 224 | pub theta: &'a str, 225 | /// Predicated delivery price. It has value when 30 min before delivery. 226 | pub predicted_delivery_price: &'a str, 227 | /// The change in the last 24 hous. 228 | pub change_24h: &'a str, 229 | } 230 | 231 | /// The future ticker data. 232 | /// 233 | /// This data utilises the snapshot field and delta field. `None` means field value has not changed. 234 | #[derive(Deserialize, Debug)] 235 | #[serde(rename_all = "camelCase")] 236 | pub struct FutureTicker<'a> { 237 | /// Symbol name. 238 | pub symbol: &'a str, 239 | /// Tick direction. 240 | pub tick_direction: Option<&'a str>, 241 | /// Percentage change of market price in the last 24 hours. 242 | pub price_24h_pcnt: Option<&'a str>, 243 | /// Last price. 244 | pub last_price: Option<&'a str>, 245 | /// Market price 24 hours ago. 246 | pub prev_price_24h: Option<&'a str>, 247 | /// The highest price in the last 24 hours. 248 | pub high_price_24h: Option<&'a str>, 249 | /// The lowest price in the last 24 hours. 250 | pub low_price_24h: Option<&'a str>, 251 | /// Market price an hour ago. 252 | pub prev_price_1h: Option<&'a str>, 253 | /// Mark price. 254 | pub mark_price: Option<&'a str>, 255 | /// Index price. 256 | pub index_price: Option<&'a str>, 257 | /// Open interest size. 258 | pub open_interest: Option<&'a str>, 259 | /// Open interest value. 260 | pub open_interest_value: Option<&'a str>, 261 | /// Turnover for 24h. 262 | pub turnover_24h: Option<&'a str>, 263 | /// Volume for 24h. 264 | pub volume_24h: Option<&'a str>, 265 | /// Next funding timestamp (ms). 266 | pub next_funding_time: Option<&'a str>, 267 | /// Funding rate. 268 | pub funding_rate: Option<&'a str>, 269 | /// Best bid price. 270 | pub bid1_price: Option<&'a str>, 271 | /// Best bid size. 272 | pub bid1_size: Option<&'a str>, 273 | /// Best ask price. 274 | pub ask1_price: Option<&'a str>, 275 | /// Best ask size. 276 | pub ask1_size: Option<&'a str>, 277 | /// Delivery date time (UTC+0). Unique field for inverse futures. 278 | pub delivery_time: Option<&'a str>, 279 | /// Delivery fee rate. Unique field for inverse futures. 280 | pub basis_rate: Option<&'a str>, 281 | /// Delivery fee rate. Unique field for inverse futures. 282 | pub delivery_fee_rate: Option<&'a str>, 283 | /// Predicated delivery price. Unique field for inverse futures. 284 | pub predicted_delivery_price: Option<&'a str>, 285 | } 286 | 287 | /// The (leveraged token) kline data. 288 | #[derive(Deserialize, Debug)] 289 | pub struct Kline<'a> { 290 | /// The start timestamp (ms) 291 | pub start: u64, 292 | /// The end timestamp (ms). It is current timestamp if it does not reach to the end time of candle. 293 | pub end: u64, 294 | /// Kline interval. 295 | pub interval: &'a str, 296 | /// Open price. 297 | pub open: &'a str, 298 | /// Close price. 299 | pub close: &'a str, 300 | /// Highest price. 301 | pub high: &'a str, 302 | /// Lowest price. 303 | pub low: &'a str, 304 | /// Trade volume. Leveraged token does not have this field. 305 | pub volume: Option<&'a str>, 306 | /// Turnover. Leveraged token does not have this field. 307 | pub turnover: Option<&'a str>, 308 | /// Weather the tick is ended or not. 309 | pub confirm: bool, 310 | /// The timestamp (ms) of the last matched order in the candle. 311 | pub timestamp: u64, 312 | } 313 | 314 | /// The liquidation data. 315 | #[derive(Deserialize, Debug)] 316 | #[serde(rename_all = "camelCase")] 317 | pub struct Liquidation<'a> { 318 | /// The updated timestamp (ms). 319 | pub updated_time: u64, 320 | /// Symbol name. 321 | pub symbol: &'a str, 322 | /// Order side. `Buy`, `Sell`. 323 | pub side: &'a str, 324 | /// Executed size. 325 | pub size: &'a str, 326 | /// Executed price. 327 | pub price: &'a str, 328 | } 329 | 330 | // The leveraged token ticker data. 331 | #[derive(Deserialize, Debug)] 332 | #[serde(rename_all = "camelCase")] 333 | pub struct LtTicker<'a> { 334 | /// Symbol name. 335 | pub symbol: &'a str, 336 | /// Market price change percentage in the past 24 hours. 337 | pub price_24h_pcnt: &'a str, 338 | /// The last price. 339 | pub last_price: &'a str, 340 | /// Market price 24 hours ago. 341 | pub prev_price_24h: &'a str, 342 | /// Highest price in the past 24 hours. 343 | pub high_price_24h: &'a str, 344 | /// Lowest price in the past 24 hours. 345 | pub low_price24h: &'a str, 346 | } 347 | 348 | /// The leveraged token nav data. 349 | #[derive(Deserialize, Debug)] 350 | #[serde(rename_all = "camelCase")] 351 | pub struct LtNav<'a> { 352 | /// The generated timestamp of nav. 353 | pub time: u64, 354 | /// Symbol name. 355 | pub symbol: &'a str, 356 | /// Net asset value. 357 | pub nav: &'a str, 358 | /// Total position value = basket value * total circulation. 359 | pub basket_position: &'a str, 360 | /// Leverage. 361 | pub leverage: &'a str, 362 | /// Basket loan. 363 | pub basket_loan: &'a str, 364 | /// Circulation. 365 | pub circulation: &'a str, 366 | /// Basket. 367 | pub basket: &'a str, 368 | } 369 | 370 | /// The position data. 371 | #[derive(Deserialize, Debug)] 372 | #[serde(rename_all = "camelCase")] 373 | pub struct Position<'a> { 374 | /// Product type. 375 | /// - Unified account: does not have this field. 376 | /// - Normal account: `linear`, `inverse`. 377 | pub category: Option<&'a str>, 378 | /// Symbol name. 379 | pub symbol: &'a str, 380 | /// Position side: `Buy`, `Sell`. 381 | pub side: &'a str, 382 | /// Position size. 383 | pub size: &'a str, 384 | /// Used to identify positions in different position modes. 385 | /// - 0 one-way mode position. 386 | /// - 1 Buy side of hedge-mode position. 387 | /// - 2 Sell side of hedge-mode position. 388 | pub position_idx: u8, 389 | /// Trade mode. 0: cross margin, 1: isolated margin. Always 0 under unified margin account. 390 | pub trade_mode: u8, 391 | /// Position value. 392 | pub position_value: &'a str, 393 | /// Risk limit ID. 394 | /// _Note_: for portfolio margin mode, it returns 0, which the risk limit value is invalid. 395 | pub risk_id: u16, 396 | /// Risk limit value corresponding to riskId. 397 | /// _Note_: for portfolio margin mode, it returns "", which the risk limit value is invalid. 398 | pub risk_limit_value: &'a str, 399 | /// Entry price. 400 | pub entry_price: &'a str, 401 | /// Mark price 402 | pub mark_price: &'a str, 403 | /// Leverage. 404 | /// _Note_: for portfolio margin mode, it returns "", which the leverage value is invalid. 405 | pub leverage: &'a str, 406 | /// Position margin. Unified account does not have this field. 407 | pub position_balance: Option<&'a str>, 408 | /// Whether to add margin automatically. 0: false, 1: true. Unified account does not have this field. 409 | pub auto_add_margin: Option, 410 | /// Position maintenance margin. 411 | /// _Note_: for portfolio margin mode, it returns "". 412 | #[serde(alias = "positionMM")] 413 | pub position_mm: &'a str, 414 | /// Position initial margin. 415 | /// _Note_: for portfolio margin mode, it returns "". 416 | #[serde(alias = "positionIM")] 417 | pub position_im: &'a str, 418 | /// Est.liquidation price. "" for Unified trade(spot/linear/options). 419 | pub liq_price: &'a str, 420 | /// Est.bankruptcy price. "" for Unified trade(spot/linear/options). 421 | pub bust_price: &'a str, 422 | /// Tp/Sl mode: `Full`, `Partial`. 423 | pub tpsl_mode: &'a str, 424 | /// Take profit price. 425 | pub take_profit: &'a str, 426 | /// Stop loss price. 427 | pub stop_loss: &'a str, 428 | /// Trailing stop. 429 | pub trailing_stop: &'a str, 430 | /// Unrealised profit and loss. 431 | pub unrealised_pnl: &'a str, 432 | /// Cumulative realised PnL. 433 | pub cum_realised_pnl: &'a str, 434 | /// Position status. 435 | /// -`Normal`. 436 | /// - `Liq`: in the liquidation progress. 437 | /// - `Adl`: in the auto-deleverage progress. 438 | pub position_status: &'a str, 439 | /// Position created timestamp (ms). 440 | pub created_time: &'a str, 441 | /// Position data updated timestamp (ms). 442 | pub updated_time: &'a str, 443 | } 444 | 445 | /// The execution data. 446 | /// 447 | /// You may have multiple executions for one order in a single message. 448 | #[derive(Deserialize, Debug)] 449 | #[serde(rename_all = "camelCase")] 450 | pub struct Execution<'a> { 451 | /// Product type. 452 | /// - Unified account: `spot`, `linear`, `option`. 453 | /// - Normal account: `linear`, `inverse`. 454 | pub category: &'a str, 455 | /// Symbol name. 456 | pub symbol: &'a str, 457 | /// Whether to borrow. Valid for `spot` only. 458 | /// - 0 (default): false. 459 | /// - 1: true. 460 | pub is_leverage: &'a str, 461 | /// Order ID. 462 | pub order_id: &'a str, 463 | /// User customized order ID. 464 | pub order_link_id: &'a str, 465 | /// Side. `Buy`, `Sell`. 466 | pub side: &'a str, 467 | /// Order price. 468 | pub order_price: &'a str, 469 | /// Order qty. 470 | pub order_qty: &'a str, 471 | /// The remaining qty not executed. 472 | pub leaves_qty: &'a str, 473 | /// Order type. `Market`, `Limit`. 474 | pub order_type: &'a str, 475 | /// Stop order type. If the order is not stop order, any type is not returned. 476 | pub stop_order_type: &'a str, 477 | /// Executed trading fee. 478 | pub exec_fee: &'a str, 479 | /// Execution ID. 480 | pub exec_id: &'a str, 481 | /// Execution price. 482 | pub exec_price: &'a str, 483 | /// Execution qty. 484 | pub exec_qty: &'a str, 485 | /// Executed type. 486 | pub exec_type: &'a str, 487 | /// Executed order value. 488 | pub exec_value: &'a str, 489 | /// Executed timestamp (ms). 490 | pub exec_time: &'a str, 491 | /// Is maker order. true: maker, false: taker. 492 | pub is_maker: bool, 493 | /// Trading fee rate. 494 | pub fee_rate: &'a str, 495 | /// Implied volatility. Valid for option. 496 | pub trade_iv: &'a str, 497 | /// Implied volatility of mark price. Valid for option. 498 | pub mark_iv: &'a str, 499 | /// The mark price of the symbol when executing. 500 | pub mark_price: &'a str, 501 | /// The index price of the symbol when executing. 502 | pub index_price: &'a str, 503 | /// The underlying price of the symbol when executing. Valid for option. 504 | pub underlying_price: &'a str, 505 | /// Paradigm block trade ID. 506 | pub block_trade_id: &'a str, 507 | } 508 | 509 | /// The order data. 510 | #[derive(Deserialize, Debug)] 511 | #[serde(rename_all = "camelCase")] 512 | pub struct Order<'a> { 513 | /// Product type. 514 | /// - Unified account: `spot`, `linear`, `option`. 515 | /// - Normal account: `linear`, `inverse`. 516 | pub category: &'a str, 517 | /// Order ID. 518 | pub order_id: &'a str, 519 | /// User customised order ID. 520 | pub order_link_id: &'a str, 521 | /// Whether to borrow. `spot` returns this field only. 0 (default): false, 1: true. 522 | pub is_leverage: &'a str, 523 | /// Block trade ID. 524 | pub block_trade_id: &'a str, 525 | /// Symbol name. 526 | pub symbol: &'a str, 527 | /// Order price. 528 | pub price: &'a str, 529 | /// Order qty. 530 | pub qty: &'a str, 531 | /// Side. `Buy`, `Sell`. 532 | pub side: &'a str, 533 | /// Position index. Used to identify positions in different position modes. 534 | pub position_idx: u8, 535 | /// Order status. 536 | pub order_status: &'a str, 537 | /// Cancel type. 538 | pub cancel_type: &'a str, 539 | /// Reject reason. 540 | pub reject_reason: &'a str, 541 | /// Average filled price. If unfilled, it is "". 542 | pub avg_price: &'a str, 543 | /// The remaining qty not executed. 544 | pub leaves_qty: &'a str, 545 | /// The remaining value not executed. 546 | pub leaves_value: &'a str, 547 | /// Cumulative executed order qty. 548 | pub cum_exec_qty: &'a str, 549 | /// Cumulative executed order value. 550 | pub cum_exec_value: &'a str, 551 | /// Cumulative executed trading fee. 552 | pub cum_exec_fee: &'a str, 553 | /// Time in force. 554 | pub time_in_force: &'a str, 555 | /// Order type. `Market`, `Limit`. 556 | pub order_type: &'a str, 557 | /// Stop order type. 558 | pub stop_order_type: &'a str, 559 | /// Implied volatility. 560 | pub order_iv: &'a str, 561 | /// Trigger price. If stopOrderType=TrailingStop, it is activate price. Otherwise, it is trigger price. 562 | pub trigger_price: &'a str, 563 | /// Take profit price. 564 | pub take_profit: &'a str, 565 | /// Stop loss price. 566 | pub stop_loss: &'a str, 567 | /// The price type to trigger take profit. 568 | pub tp_trigger_by: &'a str, 569 | /// The price type to trigger stop loss. 570 | pub sl_trigger_by: &'a str, 571 | /// Trigger direction. 1: rise, 2: fall. 572 | pub trigger_direction: u8, 573 | /// The price type of trigger price. 574 | pub trigger_by: &'a str, 575 | /// Last price when place the order. For linear only. 576 | pub last_price_on_created: &'a str, 577 | /// Reduce only. `true` means reduce position size. 578 | pub reduce_only: bool, 579 | /// Close on trigger. 580 | pub close_on_trigger: bool, 581 | /// Order created timestamp (ms). 582 | pub created_time: &'a str, 583 | /// Order updated timestamp (ms). 584 | pub updated_time: &'a str, 585 | } 586 | 587 | /// The wallet coin data. 588 | #[derive(Deserialize, Debug)] 589 | #[serde(rename_all = "camelCase")] 590 | pub struct WalletCoin<'a> { 591 | /// Coin name, such as BTC, ETH, USDT, USDC. 592 | pub coin: &'a str, 593 | /// Equity of current coin. 594 | pub equity: &'a str, 595 | /// USD value of current coin. If this coin cannot be collateral, then it is 0. 596 | pub usd_value: &'a str, 597 | /// Wallet balance of current coin. 598 | pub wallet_balance: &'a str, 599 | /// Borrow amount of current coin. 600 | pub borrow_amount: &'a str, 601 | /// Available amount to borrow of current coin. 602 | pub available_to_borrow: &'a str, 603 | /// Available amount to withdraw of current coin. 604 | pub available_to_withdraw: &'a str, 605 | /// Accrued interest. 606 | pub accrued_interest: &'a str, 607 | /// Pre-occupied margin for order. For portfolio margin mode, it returns "". 608 | #[serde(alias = "totalOrderIM")] 609 | pub total_order_im: &'a str, 610 | /// Sum of initial margin of all positions + Pre-occupied liquidation fee. For portfolio margin mode, it returns "". 611 | #[serde(alias = "totalPositionIM")] 612 | pub total_position_im: &'a str, 613 | /// Sum of maintenance margin for all positions. For portfolio margin mode, it returns "". 614 | #[serde(alias = "totalPositionMM")] 615 | pub total_position_mm: &'a str, 616 | /// Unrealised P&L. 617 | pub unrealised_pnl: &'a str, 618 | /// Cumulative Realised P&L. 619 | pub cum_realised_pnl: &'a str, 620 | } 621 | 622 | /// The wallet data. 623 | #[derive(Deserialize, Debug)] 624 | #[serde(rename_all = "camelCase")] 625 | pub struct Wallet<'a> { 626 | /// Account type. 627 | /// - Unified account: UNIFIED. 628 | /// - Normal account: CONTRACT. 629 | pub account_type: &'a str, 630 | /// Initial Margin Rate: Account Total Initial Margin Base Coin / Account Margin Balance Base Coin. 631 | /// In non-unified mode, the field will be returned as an empty string. 632 | #[serde(alias = "accountIMRate")] 633 | pub account_im_rate: &'a str, 634 | /// Maintenance Margin Rate: Account Total Maintenance Margin Base Coin / Account Margin Balance Base Coin. 635 | /// In non-unified mode, the field will be returned as an empty string. 636 | #[serde(alias = "accountMMRate")] 637 | pub account_mm_rate: &'a str, 638 | /// Equity of account converted to usd:Account Margin Balance Base Coin + Account Option Value Base Coin. 639 | /// In non-unified mode, the field will be returned as an empty string. 640 | pub total_equity: &'a str, 641 | /// Wallet Balance of account converted to usd:∑ Asset Wallet Balance By USD value of each asset. 642 | /// In non-unified mode, the field will be returned as an empty string. 643 | pub total_wallet_balance: &'a str, 644 | /// Margin Balance of account converted to usd:totalWalletBalance + totalPerpUPL. 645 | /// In non-unified mode, the field will be returned as an empty string. 646 | pub total_margin_balance: &'a str, 647 | /// Available Balance of account converted to usd:Regular mode:totalMarginBalance - totalInitialMargin. 648 | /// In non-unified mode, the field will be returned as an empty string. 649 | pub total_available_balance: &'a str, 650 | /// Unrealised P&L of perpetuals of account converted to usd:∑ Each perp upl by base coin. 651 | /// In non-unified mode, the field will be returned as an empty string. 652 | #[serde(alias = "totalPerpUPL")] 653 | pub total_perp_upl: &'a str, 654 | /// Initial Margin of account converted to usd:∑ Asset Total Initial Margin Base Coin. 655 | /// In non-unified mode, the field will be returned as an empty string. 656 | pub total_initial_margin: &'a str, 657 | /// Maintenance Margin of account converted to usd: ∑ Asset Total Maintenance Margin Base Coin. 658 | /// In non-unified mode, the field will be returned as an empty string. 659 | pub total_maintenance_margin: &'a str, 660 | /// Coin. 661 | pub coin: Vec>, 662 | } 663 | 664 | /// The greeks data. 665 | #[derive(Deserialize, Debug)] 666 | #[serde(rename_all = "camelCase")] 667 | pub struct Greek<'a> { 668 | /// Base coin. 669 | pub base_coin: &'a str, 670 | /// Delta value. 671 | pub total_delta: &'a str, 672 | /// Gamma value. 673 | pub total_gamma: &'a str, 674 | /// Vega value. 675 | pub total_vega: &'a str, 676 | /// Theta value. 677 | pub total_theta: &'a str, 678 | } 679 | 680 | #[derive(Deserialize, Debug)] 681 | #[serde(untagged)] 682 | pub enum SpotPublicResponse<'a> { 683 | #[serde(borrow)] 684 | Orderbook(BasePublicResponse<'a, Orderbook<'a>>), 685 | Trade(BasePublicResponse<'a, Vec>>), 686 | Ticker(BaseTickerPublicResponse<'a, SpotTicker<'a>>), 687 | Kline(BasePublicResponse<'a, Vec>>), 688 | LtTicker(BasePublicResponse<'a, LtTicker<'a>>), 689 | LtNav(BasePublicResponse<'a, LtNav<'a>>), 690 | Op(OpResponse<'a>), 691 | } 692 | 693 | pub struct SpotPublicResponseArg; 694 | impl Arg for SpotPublicResponseArg { 695 | type ValueType<'a> = SpotPublicResponse<'a>; 696 | } 697 | 698 | #[derive(Deserialize, Debug)] 699 | #[serde(untagged)] 700 | pub enum FuturePublicResponse<'a> { 701 | #[serde(borrow)] 702 | Orderbook(BasePublicResponse<'a, Orderbook<'a>>), 703 | Trade(BasePublicResponse<'a, Vec>>), 704 | Ticker(BaseTickerPublicResponse<'a, FutureTicker<'a>>), 705 | Kline(BasePublicResponse<'a, Vec>>), 706 | Liquidation(BasePublicResponse<'a, Liquidation<'a>>), 707 | Op(OpResponse<'a>), 708 | } 709 | 710 | pub struct FuturePublicResponseArg; 711 | impl Arg for FuturePublicResponseArg { 712 | type ValueType<'a> = FuturePublicResponse<'a>; 713 | } 714 | 715 | #[derive(Deserialize, Debug)] 716 | #[serde(untagged)] 717 | pub enum OptionPublicResponse<'a> { 718 | #[serde(borrow)] 719 | Orderbook(BaseOptionPublicResponse<'a, Orderbook<'a>>), 720 | Trade(BaseOptionPublicResponse<'a, Vec>>), 721 | Ticker(BaseOptionPublicResponse<'a, OptionTicker<'a>>), 722 | Pong(OptionPongResponse<'a>), 723 | Subscription(OptionSubscriptionResponse<'a>), 724 | } 725 | 726 | pub struct OptionPublicResponseArg; 727 | impl Arg for OptionPublicResponseArg { 728 | type ValueType<'a> = OptionPublicResponse<'a>; 729 | } 730 | 731 | #[derive(Deserialize, Debug)] 732 | #[serde(untagged)] 733 | pub enum PrivateResponse<'a> { 734 | #[serde(borrow)] 735 | Position(BasePrivateResponse<'a, Vec>>), 736 | Execution(BasePrivateResponse<'a, Vec>>), 737 | Order(BasePrivateResponse<'a, Vec>>), 738 | Wallet(BasePrivateResponse<'a, Vec>>), 739 | Greek(BasePrivateResponse<'a, Vec>>), 740 | Pong(PrivatePongResponse<'a>), 741 | Op(OpResponse<'a>), 742 | } 743 | 744 | pub struct PrivateResponseArg; 745 | impl Arg for PrivateResponseArg { 746 | type ValueType<'a> = PrivateResponse<'a>; 747 | } 748 | -------------------------------------------------------------------------------- /src/ws/spot.rs: -------------------------------------------------------------------------------- 1 | use super::callback::Callback; 2 | use super::response::SpotPublicResponseArg; 3 | use super::run; 4 | use super::Subscriber; 5 | use crate::error::Result; 6 | use crate::KlineInterval; 7 | 8 | const MAINNET_SPOT: &str = "wss://stream.bybit.com/v5/public/spot"; 9 | const TESTNET_SPOT: &str = "wss://stream-testnet.bybit.com/v5/public/spot"; 10 | 11 | pub enum OrderbookDepth { 12 | Level1, 13 | Level50, 14 | } 15 | 16 | impl From for u16 { 17 | fn from(value: OrderbookDepth) -> Self { 18 | use OrderbookDepth::*; 19 | match value { 20 | Level1 => 1, 21 | Level50 => 50, 22 | } 23 | } 24 | } 25 | 26 | pub struct SpotWebsocketApiClient { 27 | uri: String, 28 | subscriber: Subscriber, 29 | } 30 | 31 | impl SpotWebsocketApiClient { 32 | pub fn subscribe_orderbook>(&mut self, symbol: S, depth: OrderbookDepth) { 33 | self.subscriber.sub_orderbook(symbol.as_ref(), depth.into()); 34 | } 35 | 36 | pub fn subscribe_trade>(&mut self, symbol: S) { 37 | self.subscriber.sub_trade(symbol.as_ref()); 38 | } 39 | 40 | pub fn subscribe_ticker>(&mut self, symbol: S) { 41 | self.subscriber.sub_ticker(symbol.as_ref()); 42 | } 43 | 44 | pub fn subscribe_kline>(&mut self, symbol: S, interval: KlineInterval) { 45 | self.subscriber.sub_kline(symbol.as_ref(), interval.into()); 46 | } 47 | 48 | pub fn subscribe_lt_kline>(&mut self, symbol: S, interval: KlineInterval) { 49 | self.subscriber 50 | .sub_lt_kline(symbol.as_ref(), interval.into()); 51 | } 52 | 53 | pub fn subscribe_lt_ticker>(&mut self, symbol: S) { 54 | self.subscriber.sub_lt_ticker(symbol.as_ref()); 55 | } 56 | 57 | pub fn subscribe_lt_nav>(&mut self, symbol: S) { 58 | self.subscriber.sub_lt_nav(symbol.as_ref()); 59 | } 60 | 61 | pub fn run>(&self, callback: C) -> Result<()> { 62 | run(&self.uri, self.subscriber.topics(), None, callback) 63 | } 64 | } 65 | 66 | pub struct SpotWebSocketApiClientBuilder { 67 | uri: String, 68 | } 69 | 70 | impl SpotWebSocketApiClientBuilder { 71 | /// Create a new `SpotWebSocketApiClientBuilder`. Channel URI is set to the mainnet. 72 | pub fn new() -> Self { 73 | Self { 74 | uri: MAINNET_SPOT.to_string(), 75 | } 76 | } 77 | 78 | /// Change channel URI to the testnet. 79 | pub fn testnet(mut self) -> Self { 80 | self.uri = TESTNET_SPOT.to_string(); 81 | self 82 | } 83 | 84 | /// Set channel URI to the URI specified. 85 | /// 86 | /// Note URI should **match** with api client kind. 87 | /// Do not set a future channel URI to a spot api client. 88 | pub fn uri>(mut self, uri: S) -> Self { 89 | self.uri = uri.as_ref().to_owned(); 90 | self 91 | } 92 | 93 | /// Build a spot websocket api client. 94 | pub fn build(self) -> SpotWebsocketApiClient { 95 | SpotWebsocketApiClient { 96 | uri: self.uri, 97 | subscriber: Subscriber::new(), 98 | } 99 | } 100 | } 101 | --------------------------------------------------------------------------------