rusty_oms/exchanges/
bybit.rs

1//! Bybit exchange implementation
2//!
3//! This module provides order management functionality for the Bybit exchange.
4//! Note: This implementation is deprecated in favor of rusty_ems::exchanges::BybitRestClient.
5
6use std::time::{SystemTime, UNIX_EPOCH};
7
8use async_trait::async_trait;
9use hmac::{Hmac, Mac};
10use log::{debug, error};
11use reqwest::Client;
12use rusty_model::{
13    enums::{OrderSide, OrderStatus, OrderType, TimeInForce},
14    trading_order::Order,
15};
16use serde::{Deserialize, Serialize};
17use sha2::Sha256;
18use simd_json::json;
19use smallvec::SmallVec;
20
21use crate::execution_engine::Exchange;
22
23type HmacSha256 = Hmac<Sha256>;
24
25// Bybit API URLs
26const API_URL: &str = "https://api.bybit.com";
27const TESTNET_API_URL: &str = "https://api-testnet.bybit.com";
28
29/// Generic response wrapper for Bybit API
30#[derive(Debug, Serialize, Deserialize)]
31struct BybitResponse<T> {
32    /// Return code (0 for success)
33    ret_code: i32,
34    /// Return message
35    ret_msg: std::string::String,
36    /// Response result data
37    result: Option<T>,
38    /// Extended information
39    ret_ext_info: Option<simd_json::OwnedValue>,
40    /// Response timestamp
41    time: u64,
42}
43
44/// Order result structure from Bybit API
45#[derive(Debug, Serialize, Deserialize)]
46struct BybitOrderResult {
47    /// Exchange-assigned order ID
48    order_id: std::string::String,
49    /// Client-assigned order ID
50    order_link_id: std::string::String,
51    /// Trading symbol
52    symbol: std::string::String,
53    /// Order side
54    side: std::string::String,
55    /// Order type
56    order_type: std::string::String,
57    /// Order price
58    price: std::string::String,
59    /// Order quantity
60    qty: std::string::String,
61    /// Time in force
62    time_in_force: std::string::String,
63    /// Order status
64    order_status: std::string::String,
65    /// Creation timestamp
66    create_time: std::string::String,
67    /// Update timestamp
68    update_time: std::string::String,
69}
70
71/// Bybit exchange implementation
72#[deprecated(
73    note = "Use rusty_ems::exchanges::BybitRestClient instead for better performance and features"
74)]
75pub struct BybitExchange {
76    /// API key for authentication
77    pub api_key: std::string::String,
78    /// Secret key for signing requests
79    pub secret_key: std::string::String,
80    /// HTTP client for REST API calls
81    client: Client,
82    /// Whether to use testnet
83    testnet: bool,
84}
85
86#[allow(deprecated)]
87impl BybitExchange {
88    /// Create a new Bybit exchange instance
89    #[must_use]
90    pub fn new(api_key: std::string::String, secret_key: std::string::String) -> Self {
91        Self {
92            api_key,
93            secret_key,
94            client: Client::new(),
95            testnet: false,
96        }
97    }
98
99    /// Create a new Bybit exchange instance with testnet flag
100    #[must_use]
101    pub fn with_testnet(
102        api_key: std::string::String,
103        secret_key: std::string::String,
104        testnet: bool,
105    ) -> Self {
106        Self {
107            api_key,
108            secret_key,
109            client: Client::new(),
110            testnet,
111        }
112    }
113
114    /// Get the base URL based on testnet flag
115    const fn get_base_url(&self) -> &'static str {
116        if self.testnet {
117            TESTNET_API_URL
118        } else {
119            API_URL
120        }
121    }
122
123    /// Map Bybit order status to internal `OrderStatus`
124    fn map_order_status(status: &str) -> OrderStatus {
125        match status {
126            "Created" | "New" => OrderStatus::New,
127            "PartiallyFilled" => OrderStatus::PartiallyFilled,
128            "Filled" => OrderStatus::Filled,
129            "Cancelled" | "Rejected" => OrderStatus::Cancelled, // Use the correct variant spelling
130            "PendingCancel" => OrderStatus::PendingCancel,
131            _ => OrderStatus::New,
132        }
133    }
134
135    /// Map internal `OrderType` to Bybit order type
136    const fn map_order_type(order_type: OrderType) -> &'static str {
137        match order_type {
138            OrderType::Limit => "Limit",
139            OrderType::Market => "Market",
140            _ => "Limit", // Default to limit for unsupported types
141        }
142    }
143
144    /// Map internal `OrderSide` to Bybit order side
145    const fn map_order_side(side: OrderSide) -> &'static str {
146        match side {
147            OrderSide::Buy => "Buy",
148            OrderSide::Sell => "Sell",
149        }
150    }
151
152    /// Map internal `TimeInForce` to Bybit time in force
153    const fn map_time_in_force(tif: TimeInForce) -> &'static str {
154        match tif {
155            TimeInForce::GTC => "GoodTillCancel",
156            TimeInForce::IOC => "ImmediateOrCancel",
157            TimeInForce::FOK => "FillOrKill",
158            _ => "GoodTillCancel", // Default to GTC for unsupported types
159        }
160    }
161
162    /// Generate signature for Bybit API requests
163    fn generate_signature(&self, timestamp: u64, params: &str) -> std::string::String {
164        let string_to_sign = format!("{}{}{}", timestamp, self.api_key, params);
165        let mut mac = HmacSha256::new_from_slice(self.secret_key.as_bytes())
166            .expect("HMAC can take key of any size");
167        mac.update(string_to_sign.as_bytes());
168        let result = mac.finalize();
169        hex::encode(result.into_bytes())
170    }
171
172    /// Get current timestamp in milliseconds
173    fn get_timestamp() -> u64 {
174        SystemTime::now()
175            .duration_since(UNIX_EPOCH)
176            .expect("Time went backwards")
177            .as_millis() as u64
178    }
179
180    /// Function to extract `time_in_force` from `order_type`
181    const fn get_time_in_force(&self, order_type: OrderType) -> TimeInForce {
182        match order_type {
183            OrderType::Market => TimeInForce::IOC,
184            OrderType::Limit => TimeInForce::GTC,
185            _ => TimeInForce::GTC,
186        }
187    }
188}
189
190#[async_trait]
191#[allow(deprecated)]
192impl Exchange for BybitExchange {
193    async fn send_order(&self, order: Order) -> crate::Result<()> {
194        let timestamp = Self::get_timestamp();
195        let base_url = self.get_base_url();
196
197        // Determine the category based on the instrument type
198        // For simplicity, we'll assume spot trading here
199        let category = "spot";
200
201        // Construct the request body
202        let body = json!({
203            "category": category,
204            "symbol": order.symbol.clone(),
205            "side": Self::map_order_side(order.side),
206            "orderType": Self::map_order_type(order.order_type),
207            "qty": order.quantity.to_string(),
208            "price": order.price.map_or_else(|| "0".to_string(), |p| p.to_string()),
209            "timeInForce": Self::map_time_in_force(self.get_time_in_force(order.order_type)),
210            "orderLinkId": order.id.clone(), // Using order ID as order link ID
211        });
212
213        let body_str = simd_json::to_string(&body)
214            .map_err(|e| crate::OmsError::Exchange(format!("Failed to serialize: {e}").into()))?;
215
216        // Generate signature
217        let signature = self.generate_signature(timestamp, &body_str);
218
219        // Construct the URL
220        let url = format!("{base_url}/v5/order/create");
221
222        // Send the request
223        let response = match self
224            .client
225            .post(&url)
226            .header("X-BAPI-API-KEY", self.api_key.as_str())
227            .header("X-BAPI-TIMESTAMP", timestamp.to_string().as_str())
228            .header("X-BAPI-SIGN", signature.as_str())
229            .header("Content-Type", "application/json")
230            .body(body_str)
231            .send()
232            .await
233        {
234            Ok(resp) => resp,
235            Err(e) => {
236                return Err(crate::OmsError::Exchange(
237                    format!("Request error: {e}").into(),
238                ));
239            }
240        };
241
242        let response_text = match response.text().await {
243            Ok(text) => text,
244            Err(e) => {
245                return Err(crate::OmsError::Exchange(
246                    format!("Failed to read response: {e}").into(),
247                ));
248            }
249        };
250
251        // Keep original response text for error messages before converting to bytes
252        let original_response_text = response_text.clone();
253        let mut response_bytes = response_text.into_bytes();
254        let response_json: BybitResponse<BybitOrderResult> =
255            match simd_json::from_slice(&mut response_bytes) {
256                Ok(json) => json,
257                Err(e) => {
258                    // Use original response text to avoid corrupted buffer in error message
259                    return Err(crate::OmsError::Exchange(
260                        format!("Failed to parse response: {e}: {original_response_text}").into(),
261                    ));
262                }
263            };
264
265        if response_json.ret_code != 0 {
266            let error_message = format!(
267                "Bybit order placement failed: {} (code: {})",
268                response_json.ret_msg, response_json.ret_code
269            );
270            error!("{error_message}");
271            return Err(crate::OmsError::Exchange(error_message.into()));
272        }
273
274        debug!("Order placed successfully: {}", order.id);
275        Ok(())
276    }
277
278    async fn cancel_order(&self, order_id: std::string::String) -> crate::Result<()> {
279        let timestamp = Self::get_timestamp();
280        let base_url = self.get_base_url();
281
282        // In a real implementation, we would need to know the symbol and category
283        // For simplicity, we'll assume the order_id contains the necessary information
284        // Format: CATEGORY:SYMBOL:ORDER_LINK_ID
285        let parts: SmallVec<[&str; 3]> = order_id.split(':').collect();
286        if parts.len() < 3 {
287            return Err(crate::OmsError::Exchange(
288                "Invalid order_id format. Expected CATEGORY:SYMBOL:ORDER_LINK_ID".into(),
289            ));
290        }
291
292        let category = parts[0];
293        let symbol = parts[1];
294        let order_link_id = parts[2];
295
296        // Construct the request body
297        let body = json!({
298            "category": category,
299            "symbol": symbol,
300            "orderLinkId": order_link_id,
301        });
302
303        let body_str = simd_json::to_string(&body)
304            .map_err(|e| crate::OmsError::Exchange(format!("Failed to serialize: {e}").into()))?;
305
306        // Generate signature
307        let signature = self.generate_signature(timestamp, &body_str);
308
309        // Construct the URL
310        let url = format!("{base_url}/v5/order/cancel");
311
312        // Send the request
313        let response = match self
314            .client
315            .post(&url)
316            .header("X-BAPI-API-KEY", self.api_key.as_str())
317            .header("X-BAPI-TIMESTAMP", timestamp.to_string().as_str())
318            .header("X-BAPI-SIGN", signature.as_str())
319            .header("Content-Type", "application/json")
320            .body(body_str)
321            .send()
322            .await
323        {
324            Ok(resp) => resp,
325            Err(e) => {
326                return Err(crate::OmsError::Exchange(
327                    format!("Request error: {e}").into(),
328                ));
329            }
330        };
331
332        let response_text = match response.text().await {
333            Ok(text) => text,
334            Err(e) => {
335                return Err(crate::OmsError::Exchange(
336                    format!("Failed to read response: {e}").into(),
337                ));
338            }
339        };
340
341        // Keep original response text for error messages before converting to bytes
342        let original_response_text = response_text.clone();
343        let mut response_bytes = response_text.into_bytes();
344        let response_json: BybitResponse<BybitOrderResult> =
345            match simd_json::from_slice(&mut response_bytes) {
346                Ok(json) => json,
347                Err(e) => {
348                    // Use original response text to avoid corrupted buffer in error message
349                    return Err(crate::OmsError::Exchange(
350                        format!("Failed to parse response: {e}: {original_response_text}").into(),
351                    ));
352                }
353            };
354
355        if response_json.ret_code != 0 {
356            let error_message = format!(
357                "Bybit order cancellation failed: {} (code: {})",
358                response_json.ret_msg, response_json.ret_code
359            );
360            error!("{error_message}");
361            return Err(crate::OmsError::Exchange(error_message.into()));
362        }
363
364        debug!("Order cancelled successfully: {}", order_id);
365        Ok(())
366    }
367
368    async fn get_order_status(
369        &self,
370        order_id: std::string::String,
371    ) -> crate::Result<std::string::String> {
372        let timestamp = Self::get_timestamp();
373        let base_url = self.get_base_url();
374
375        // In a real implementation, we would need to know the symbol and category
376        // For simplicity, we'll assume the order_id contains the necessary information
377        // Format: CATEGORY:SYMBOL:ORDER_LINK_ID
378        let parts: SmallVec<[&str; 3]> = order_id.split(':').collect();
379        if parts.len() < 3 {
380            return Err(crate::OmsError::Exchange(
381                "Invalid order_id format. Expected CATEGORY:SYMBOL:ORDER_LINK_ID".into(),
382            ));
383        }
384
385        let category = parts[0];
386        let symbol = parts[1];
387        let order_link_id = parts[2];
388
389        // Construct the query parameters
390        let params = format!("category={category}&symbol={symbol}&orderLinkId={order_link_id}");
391
392        // Generate signature
393        let signature = self.generate_signature(timestamp, &params);
394
395        // Construct the URL
396        let url = format!("{base_url}/v5/order/realtime?{params}");
397
398        // Send the request
399        let response = match self
400            .client
401            .get(&url)
402            .header("X-BAPI-API-KEY", self.api_key.as_str())
403            .header("X-BAPI-TIMESTAMP", timestamp.to_string().as_str())
404            .header("X-BAPI-SIGN", signature.as_str())
405            .send()
406            .await
407        {
408            Ok(resp) => resp,
409            Err(e) => {
410                return Err(crate::OmsError::Exchange(
411                    format!("Request error: {e}").into(),
412                ));
413            }
414        };
415
416        let response_text = match response.text().await {
417            Ok(text) => text,
418            Err(e) => {
419                return Err(crate::OmsError::Exchange(
420                    format!("Failed to read response: {e}").into(),
421                ));
422            }
423        };
424
425        #[derive(Debug, Serialize, Deserialize)]
426        struct OrderList {
427            list: Vec<BybitOrderResult>,
428        }
429
430        // Keep original response text for error messages before converting to bytes
431        let original_response_text = response_text.clone();
432        let mut response_bytes = response_text.into_bytes();
433        let response_json: BybitResponse<OrderList> =
434            match simd_json::from_slice(&mut response_bytes) {
435                Ok(json) => json,
436                Err(e) => {
437                    // Use original response text to avoid corrupted buffer in error message
438                    return Err(crate::OmsError::Exchange(
439                        format!("Failed to parse response: {e}: {original_response_text}").into(),
440                    ));
441                }
442            };
443
444        if response_json.ret_code != 0 {
445            let error_message = format!(
446                "Bybit get order status failed: {} (code: {})",
447                response_json.ret_msg, response_json.ret_code
448            );
449            error!("{error_message}");
450            return Err(crate::OmsError::Exchange(error_message.into()));
451        }
452
453        if let Some(result) = response_json.result
454            && let Some(order) = result.list.first()
455        {
456            return Ok(order.order_status.clone());
457        }
458
459        Err(crate::OmsError::OrderNotFound("Order not found".into()))
460    }
461}