1use 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
25const API_URL: &str = "https://api.bybit.com";
27const TESTNET_API_URL: &str = "https://api-testnet.bybit.com";
28
29#[derive(Debug, Serialize, Deserialize)]
31struct BybitResponse<T> {
32 ret_code: i32,
34 ret_msg: std::string::String,
36 result: Option<T>,
38 ret_ext_info: Option<simd_json::OwnedValue>,
40 time: u64,
42}
43
44#[derive(Debug, Serialize, Deserialize)]
46struct BybitOrderResult {
47 order_id: std::string::String,
49 order_link_id: std::string::String,
51 symbol: std::string::String,
53 side: std::string::String,
55 order_type: std::string::String,
57 price: std::string::String,
59 qty: std::string::String,
61 time_in_force: std::string::String,
63 order_status: std::string::String,
65 create_time: std::string::String,
67 update_time: std::string::String,
69}
70
71#[deprecated(
73 note = "Use rusty_ems::exchanges::BybitRestClient instead for better performance and features"
74)]
75pub struct BybitExchange {
76 pub api_key: std::string::String,
78 pub secret_key: std::string::String,
80 client: Client,
82 testnet: bool,
84}
85
86#[allow(deprecated)]
87impl BybitExchange {
88 #[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 #[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 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 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, "PendingCancel" => OrderStatus::PendingCancel,
131 _ => OrderStatus::New,
132 }
133 }
134
135 const fn map_order_type(order_type: OrderType) -> &'static str {
137 match order_type {
138 OrderType::Limit => "Limit",
139 OrderType::Market => "Market",
140 _ => "Limit", }
142 }
143
144 const fn map_order_side(side: OrderSide) -> &'static str {
146 match side {
147 OrderSide::Buy => "Buy",
148 OrderSide::Sell => "Sell",
149 }
150 }
151
152 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", }
160 }
161
162 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 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 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 let category = "spot";
200
201 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(), });
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 let signature = self.generate_signature(timestamp, &body_str);
218
219 let url = format!("{base_url}/v5/order/create");
221
222 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 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 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 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 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 let signature = self.generate_signature(timestamp, &body_str);
308
309 let url = format!("{base_url}/v5/order/cancel");
311
312 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 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 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 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 let params = format!("category={category}&symbol={symbol}&orderLinkId={order_link_id}");
391
392 let signature = self.generate_signature(timestamp, ¶ms);
394
395 let url = format!("{base_url}/v5/order/realtime?{params}");
397
398 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 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 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}