Skip to main content

bybit_rust_api/ws/
trade.rs

1//! WebSocket Trade (Order Entry) — place, amend, cancel orders via WebSocket.
2//!
3//! Bybit V5 supports order entry through WebSocket for lower latency
4//! compared to REST API. All operations require authentication.
5//!
6//! # Topics / Operations
7//! - `order.create` — Place a new order
8//! - `order.amend`  — Amend an existing order
9//! - `order.cancel` — Cancel an order
10//!
11//! # Usage
12//!
13//! ```ignore
14//! client.place_order_via_ws(PlaceOrderRequest { ... }).await?;
15//! ```
16
17use crate::dto::{AmendOrderRequest, CancelOrderRequest, PlaceOrderRequest};
18use serde::{Deserialize, Serialize};
19use serde_json::Value;
20
21// ── Request Types ──────────────────────────────────────────────
22
23/// Order operation types for WebSocket.
24#[derive(Debug, Clone, Serialize)]
25#[serde(rename_all = "snake_case")]
26pub enum TradeOp {
27    OrderCreate,
28    OrderAmend,
29    OrderCancel,
30}
31
32/// Request header for WS trade operations.
33#[derive(Debug, Clone, Serialize)]
34pub struct WsTradeRequest {
35    /// Request ID (unique per request, echoed in response)
36    #[serde(rename = "reqId")]
37    pub req_id: String,
38    /// Header with operation type
39    pub header: TradeHeader,
40    /// Request parameters (order details)
41    pub args: Vec<Value>,
42}
43
44#[derive(Debug, Clone, Serialize)]
45pub struct TradeHeader {
46    /// X-BAPI-TIMESTAMP
47    #[serde(rename = "X-BAPI-TIMESTAMP")]
48    pub timestamp: String,
49    /// X-BAPI-RECV-WINDOW
50    #[serde(rename = "X-BAPI-RECV-WINDOW")]
51    pub recv_window: String,
52    /// X-BAPI-API-KEY
53    #[serde(rename = "X-BAPI-API-KEY")]
54    pub api_key: String,
55    /// X-BAPI-SIGN
56    #[serde(rename = "X-BAPI-SIGN")]
57    pub signature: String,
58}
59
60// ── Response Types ─────────────────────────────────────────────
61
62/// Response for WS order operations.
63#[derive(Debug, Clone, Deserialize)]
64pub struct WsTradeResponse {
65    /// Request ID (matches the request)
66    #[serde(rename = "reqId")]
67    #[serde(default)]
68    pub req_id: Option<String>,
69    /// Operation type echoed back
70    #[serde(rename = "op")]
71    #[serde(default)]
72    pub op: Option<String>,
73    /// Return code (0 = success)
74    #[serde(rename = "retCode")]
75    #[serde(default)]
76    pub ret_code: Option<i32>,
77    /// Return message
78    #[serde(rename = "retMsg")]
79    #[serde(default)]
80    pub ret_msg: Option<String>,
81    /// Whether the operation succeeded
82    #[serde(default)]
83    pub success: Option<bool>,
84    /// Connection ID
85    #[serde(rename = "connId")]
86    #[serde(default)]
87    pub conn_id: Option<String>,
88    /// The result data
89    #[serde(default)]
90    pub data: Option<TradeResultData>,
91}
92
93/// Result data from a WS trade operation.
94#[derive(Debug, Clone, Deserialize)]
95pub struct TradeResultData {
96    /// Order ID
97    #[serde(rename = "orderId")]
98    #[serde(default)]
99    pub order_id: Option<String>,
100    /// Client-specified order link ID
101    #[serde(rename = "orderLinkId")]
102    #[serde(default)]
103    pub order_link_id: Option<String>,
104    /// Order status
105    #[serde(rename = "orderStatus")]
106    #[serde(default)]
107    pub order_status: Option<String>,
108    /// Category
109    #[serde(default)]
110    pub category: Option<String>,
111    /// Symbol
112    #[serde(default)]
113    pub symbol: Option<String>,
114}
115
116// ── Builder / Constructors ─────────────────────────────────────
117
118impl WsTradeRequest {
119    /// Generate a unique request ID based on timestamp.
120    pub fn new_req_id() -> String {
121        use std::time::{SystemTime, UNIX_EPOCH};
122        let ts = SystemTime::now()
123            .duration_since(UNIX_EPOCH)
124            .unwrap()
125            .as_micros();
126        format!("ws-{}", ts)
127    }
128
129    /// Build a place-order WS request with signature.
130    pub fn create_order(
131        order: PlaceOrderRequest,
132        api_key: &str,
133        api_secret: &str,
134        recv_window: u64,
135    ) -> Self {
136        let req_id = Self::new_req_id();
137        let timestamp = crate::utils::millis().to_string();
138        let body = serde_json::to_value(&order).unwrap();
139
140        // Signature: HMAC-SHA256(timestamp + api_key + recv_window + body_json)
141        let signature_input = format!(
142            "{}{}{}{}",
143            timestamp,
144            api_key,
145            recv_window,
146            serde_json::to_string(&body).unwrap()
147        );
148        let signature = crate::utils::sign(api_secret, &signature_input);
149
150        WsTradeRequest {
151            req_id,
152            header: TradeHeader {
153                timestamp,
154                recv_window: recv_window.to_string(),
155                api_key: api_key.to_string(),
156                signature,
157            },
158            args: vec![body],
159        }
160    }
161
162    /// Build an amend-order WS request with signature.
163    pub fn amend_order(
164        amend: AmendOrderRequest,
165        api_key: &str,
166        api_secret: &str,
167        recv_window: u64,
168    ) -> Self {
169        let req_id = Self::new_req_id();
170        let timestamp = crate::utils::millis().to_string();
171        let body = serde_json::to_value(&amend).unwrap();
172        let signature_input = format!(
173            "{}{}{}{}",
174            timestamp,
175            api_key,
176            recv_window,
177            serde_json::to_string(&body).unwrap()
178        );
179        let signature = crate::utils::sign(api_secret, &signature_input);
180
181        WsTradeRequest {
182            req_id,
183            header: TradeHeader {
184                timestamp,
185                recv_window: recv_window.to_string(),
186                api_key: api_key.to_string(),
187                signature,
188            },
189            args: vec![body],
190        }
191    }
192
193    /// Build a cancel-order WS request with signature.
194    pub fn cancel_order(
195        cancel: CancelOrderRequest,
196        api_key: &str,
197        api_secret: &str,
198        recv_window: u64,
199    ) -> Self {
200        let req_id = Self::new_req_id();
201        let timestamp = crate::utils::millis().to_string();
202        let body = serde_json::to_value(&cancel).unwrap();
203        let signature_input = format!(
204            "{}{}{}{}",
205            timestamp,
206            api_key,
207            recv_window,
208            serde_json::to_string(&body).unwrap()
209        );
210        let signature = crate::utils::sign(api_secret, &signature_input);
211
212        WsTradeRequest {
213            req_id,
214            header: TradeHeader {
215                timestamp,
216                recv_window: recv_window.to_string(),
217                api_key: api_key.to_string(),
218                signature,
219            },
220            args: vec![body],
221        }
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228    use crate::dto::PlaceOrderRequest;
229    use crate::rest::enums::{Category, OrderType, Side, TimeInForce};
230
231    #[test]
232    fn test_create_order_request() {
233        let order = PlaceOrderRequest {
234            category: Category::Spot,
235            symbol: "BTCUSDT".to_string(),
236            side: Side::Buy,
237            order_type: OrderType::Limit,
238            qty: "0.001".to_string(),
239            price: Some("40000".to_string()),
240            time_in_force: Some(TimeInForce::GTC),
241            ..Default::default()
242        };
243
244        let req = WsTradeRequest::create_order(order, "key", "secret", 5000);
245        assert!(req.req_id.starts_with("ws-"));
246        assert_eq!(req.args.len(), 1);
247    }
248
249    #[test]
250    fn test_req_id_unique() {
251        let id1 = WsTradeRequest::new_req_id();
252        std::thread::sleep(std::time::Duration::from_micros(10));
253        let id2 = WsTradeRequest::new_req_id();
254        assert_ne!(id1, id2);
255    }
256
257    #[test]
258    fn test_deserialize_trade_response() {
259        let json = serde_json::json!({
260            "reqId": "ws-123",
261            "op": "order.create",
262            "retCode": 0,
263            "retMsg": "OK",
264            "success": true,
265            "data": {
266                "orderId": "order-456",
267                "orderLinkId": "link-789",
268                "orderStatus": "New",
269                "category": "spot",
270                "symbol": "BTCUSDT"
271            }
272        });
273
274        let resp: WsTradeResponse = serde_json::from_value(json).unwrap();
275        assert_eq!(resp.ret_code, Some(0));
276        assert_eq!(resp.success, Some(true));
277        assert_eq!(
278            resp.data.as_ref().unwrap().order_id.as_deref(),
279            Some("order-456")
280        );
281    }
282}