1use std::collections::HashMap;
16use std::sync::{Arc, Mutex};
17use std::time::Duration;
18
19use reqwest::header::HeaderMap;
20use serde_json::{json, Value};
21
22use crate::core::{
23 HttpClient, Credentials, assemble_rest_url,
24 ExchangeId, ExchangeType, AccountType, Symbol,
25 ExchangeError, ExchangeResult,
26 Price, Kline, Ticker, OrderBook,
27 Order, OrderSide, OrderType, Balance, AccountInfo,
28 Position, FundingRate,
29 OrderRequest, CancelRequest, CancelScope,
30 BalanceQuery, PositionQuery, PositionModification,
31 OrderHistoryFilter, PlaceOrderResponse, FeeInfo,
32 UserTrade, UserTradeFilter,
33 MarginType,
34 AmendRequest, CancelAllResponse, OrderResult,
35 TransferResponse, DepositAddress, WithdrawResponse, FundsRecord,
36 SymbolInput,
37};
38use crate::core::types::{
39 TransferRequest, TransferHistoryFilter,
40 WithdrawRequest, FundsHistoryFilter, FundsRecordType,
41 SubAccountOperation, SubAccountResult,
42 OpenInterest, LongShortRatio, MarkPrice,
43};
44use crate::core::traits::{
45 ExchangeIdentity, MarketData, Trading, Account, Positions,
46 CancelAll, AmendOrder, BatchOrders,
47 AccountTransfers, CustodialFunds, SubAccounts,
48 FundingHistory, AccountLedger,
49 MarketDataPublic,
50};
51use crate::core::types::{
52 ConnectorStats,
53 FundingPayment, FundingFilter,
54 LedgerEntry, LedgerFilter,
55 MarketDataCapabilities, TradingCapabilities, AccountCapabilities,
56};
57use crate::core::utils::{RuntimeLimiter, RateLimitMonitor, RateLimitPressure};
58use crate::core::types::{RateLimitCapabilities, LimitModel, RestLimitPool, WsLimits, OrderbookCapabilities, WsBookChannel};
59
60use super::endpoints::{BybitUrls, BybitEndpoint, format_symbol, account_type_to_category, account_type_to_transfer_type, map_kline_interval};
61use super::auth::BybitAuth;
62use super::parser::BybitParser;
63
64static BYBIT_POOLS: &[RestLimitPool] = &[RestLimitPool {
69 name: "default",
70 max_budget: 600,
71 window_seconds: 5,
72 is_weight: true,
73 has_server_headers: true,
74 server_header: Some("X-Bapi-Limit-Status"),
75 header_reports_used: false,
76}];
77
78static BYBIT_RATE_CAPS: RateLimitCapabilities = RateLimitCapabilities {
79 model: LimitModel::Weight,
80 rest_pools: BYBIT_POOLS,
81 decaying: None,
82 endpoint_weights: &[],
83 ws: WsLimits {
84 max_connections: None,
85 max_subs_per_conn: None,
86 max_msg_per_sec: None,
87 max_streams_per_conn: None,
88 },
89};
90
91pub struct BybitConnector {
97 http: HttpClient,
99 auth: Option<BybitAuth>,
101 testnet: bool,
103 rest_override: Option<String>,
106 limiter: Arc<Mutex<RuntimeLimiter>>,
108 monitor: Arc<Mutex<RateLimitMonitor>>,
110 precision: crate::core::utils::precision::PrecisionCache,
112}
113
114impl BybitConnector {
115 pub async fn new(credentials: Option<Credentials>, testnet: bool) -> ExchangeResult<Self> {
117 Self::new_with_override(credentials, testnet, None).await
118 }
119
120 pub async fn new_with_override(credentials: Option<Credentials>, testnet: bool, rest_override: Option<String>) -> ExchangeResult<Self> {
125 let http = HttpClient::new(30_000)?; let mut auth = credentials.as_ref().map(BybitAuth::new);
128
129 if auth.is_some() {
131 let base_url = BybitUrls::base_url(testnet);
132 let url = format!("{}/v5/market/time", base_url);
133 if let Ok(response) = http.get(&url, &HashMap::new()).await {
134 if let Some(time_sec) = response.get("result")
135 .and_then(|r| r.get("timeSecond"))
136 .and_then(|t| t.as_str())
137 .and_then(|s| s.parse::<i64>().ok())
138 {
139 if let Some(ref mut a) = auth {
140 a.sync_time(time_sec * 1000); }
142 }
143 }
144 }
145
146 let limiter = Arc::new(Mutex::new(RuntimeLimiter::from_caps(&BYBIT_RATE_CAPS)));
147 let monitor = Arc::new(Mutex::new(RateLimitMonitor::new("Bybit")));
148
149 Ok(Self {
150 http,
151 auth,
152 testnet,
153 rest_override,
154 limiter,
155 monitor,
156 precision: crate::core::utils::precision::PrecisionCache::new(),
157 })
158 }
159
160 pub async fn public(testnet: bool, rest_override: Option<String>) -> ExchangeResult<Self> {
162 Self::new_with_override(None, testnet, rest_override).await
163 }
164
165
166 fn update_rate_from_headers(&self, headers: &HeaderMap) {
174 let remaining = headers
175 .get("X-Bapi-Limit-Status")
176 .and_then(|v| v.to_str().ok())
177 .and_then(|s| s.parse::<u32>().ok());
178
179 let limit = headers
180 .get("X-Bapi-Limit")
181 .and_then(|v| v.to_str().ok())
182 .and_then(|s| s.parse::<u32>().ok());
183
184 if let (Some(remaining), Some(limit)) = (remaining, limit) {
185 let used = limit.saturating_sub(remaining);
186 if let Ok(mut limiter) = self.limiter.lock() {
187 limiter.update_from_server("default", used);
188 }
189 }
190 }
191
192 async fn rate_limit_wait(&self, weight: u32, essential: bool) -> bool {
197 loop {
198 let wait_time = {
199 let mut limiter = self.limiter.lock()
200 .expect("rate limiter mutex poisoned");
201
202 let pressure = self.monitor.lock()
203 .expect("rate monitor mutex poisoned")
204 .check(&mut limiter);
205 if pressure >= RateLimitPressure::Cutoff && !essential {
206 return false;
207 }
208
209 if limiter.try_acquire("default", weight) {
210 return true;
211 }
212 limiter.time_until_ready("default", weight)
213 };
214 if wait_time > Duration::ZERO {
215 tokio::time::sleep(wait_time).await;
216 }
217 }
218 }
219
220 async fn get(
222 &self,
223 endpoint: BybitEndpoint,
224 params: HashMap<String, String>,
225 ) -> ExchangeResult<Value> {
226 if !self.rate_limit_wait(1, false).await {
228 return Err(ExchangeError::RateLimitExceeded {
229 retry_after: None,
230 message: "Rate limit budget >= 90% used; non-essential market data request dropped".to_string(),
231 });
232 }
233
234 let real_base = BybitUrls::base_url(self.testnet);
235 let path = endpoint.path();
236
237 let query = if params.is_empty() {
239 String::new()
240 } else {
241 let qs: Vec<String> = params.iter()
242 .map(|(k, v)| format!("{}={}", k, v))
243 .collect();
244 qs.join("&")
245 };
246
247 let query_sfx = if query.is_empty() { String::new() } else { format!("?{}", query) };
248 let url = assemble_rest_url(self.rest_override.as_deref(), real_base, path, &query_sfx);
249
250 let headers = if endpoint.is_private() {
252 let auth = self.auth.as_ref()
253 .ok_or_else(|| ExchangeError::Auth("Authentication required".to_string()))?;
254 auth.sign_request("GET", &query)
255 } else {
256 HashMap::new()
257 };
258
259 let (response, resp_headers) = self.http.get_with_response_headers(&url, &HashMap::new(), &headers).await?;
260 self.update_rate_from_headers(&resp_headers);
261 self.check_response(&response)?;
262 Ok(response)
263 }
264
265 async fn post(
267 &self,
268 endpoint: BybitEndpoint,
269 body: Value,
270 ) -> ExchangeResult<Value> {
271 self.rate_limit_wait(1, true).await;
273
274 let real_base = BybitUrls::base_url(self.testnet);
275 let path = endpoint.path();
276 let url = assemble_rest_url(self.rest_override.as_deref(), real_base, path, "");
277
278 let auth = self.auth.as_ref()
280 .ok_or_else(|| ExchangeError::Auth("Authentication required".to_string()))?;
281 let body_str = body.to_string();
282 let headers = auth.sign_request("POST", &body_str);
283
284 let (response, resp_headers) = self.http.post_with_response_headers(&url, &body, &headers).await?;
285 self.update_rate_from_headers(&resp_headers);
286 self.check_response(&response)?;
287 Ok(response)
288 }
289
290 fn check_response(&self, response: &Value) -> ExchangeResult<()> {
292 let ret_code = response.get("retCode")
293 .and_then(|c| c.as_i64())
294 .unwrap_or(-1);
295
296 if ret_code != 0 {
297 let msg = response.get("retMsg")
298 .and_then(|m| m.as_str())
299 .unwrap_or("Unknown error");
300 return Err(ExchangeError::Api {
301 code: ret_code as i32,
302 message: msg.to_string(),
303 });
304 }
305
306 Ok(())
307 }
308
309 pub async fn get_all_tickers(&self, account_type: AccountType) -> ExchangeResult<Vec<Ticker>> {
315 let mut params = HashMap::new();
316 params.insert("category".to_string(), account_type_to_category(account_type).to_string());
317
318 let response = self.get(BybitEndpoint::Ticker, params).await?;
319
320 let result = response.get("result")
321 .ok_or_else(|| ExchangeError::Parse("Missing 'result' field".to_string()))?;
322 let list = result.get("list")
323 .and_then(|v| v.as_array())
324 .ok_or_else(|| ExchangeError::Parse("Missing 'result.list' array".to_string()))?;
325
326 let timestamp = response.get("time").and_then(|t| t.as_i64()).unwrap_or(0);
327
328 let tickers = list.iter().map(|data| {
329 let parse_str_f64 = |key: &str| -> Option<f64> {
330 data.get(key).and_then(|v| v.as_str()).and_then(|s| s.parse().ok())
331 };
332
333 let last_price = parse_str_f64("lastPrice").unwrap_or(0.0);
334 let prev_price = parse_str_f64("prevPrice24h");
335 let price_change_24h = prev_price.map(|p| last_price - p);
336 let price_change_percent_24h = data.get("price24hPcnt")
337 .and_then(|v| v.as_str())
338 .and_then(|s| s.parse::<f64>().ok())
339 .map(|v| v * 100.0);
340
341 Ticker {
342 last_price,
343 bid_price: parse_str_f64("bid1Price"),
344 ask_price: parse_str_f64("ask1Price"),
345 high_24h: parse_str_f64("highPrice24h"),
346 low_24h: parse_str_f64("lowPrice24h"),
347 volume_24h: parse_str_f64("volume24h"),
348 quote_volume_24h: parse_str_f64("turnover24h"),
349 price_change_24h,
350 price_change_percent_24h,
351 timestamp,
352 }
353 }).collect();
354
355 Ok(tickers)
356 }
357
358 pub async fn get_symbols(&self, account_type: AccountType) -> ExchangeResult<Value> {
360 let mut params = HashMap::new();
361 params.insert("category".to_string(), account_type_to_category(account_type).to_string());
362
363 self.get(BybitEndpoint::Symbols, params).await
364 }
365
366 pub async fn cancel_all_orders(
368 &self,
369 symbol: Option<Symbol>,
370 account_type: AccountType,
371 ) -> ExchangeResult<Vec<String>> {
372 let mut body = json!({
373 "category": account_type_to_category(account_type),
374 });
375
376 if let Some(s) = symbol {
377 body["symbol"] = json!(format_symbol(&s, account_type));
378 }
379
380 let response = self.post(BybitEndpoint::CancelAllOrders, body).await?;
381
382 let result = response.get("result")
384 .ok_or_else(|| ExchangeError::Parse("Missing result".to_string()))?;
385
386 let ids = result.get("list")
387 .and_then(|v| v.as_array())
388 .map(|arr| {
389 arr.iter()
390 .filter_map(|v| v.get("orderId").and_then(|id| id.as_str()).map(String::from))
391 .collect()
392 })
393 .unwrap_or_default();
394
395 Ok(ids)
396 }
397
398 pub async fn get_open_interest(
407 &self,
408 category: &str,
409 symbol: &str,
410 interval_time: &str,
411 limit: Option<u32>,
412 start_time: Option<i64>,
413 end_time: Option<i64>,
414 ) -> ExchangeResult<Vec<crate::core::types::OpenInterest>> {
415 let mut params = HashMap::new();
416 params.insert("category".to_string(), category.to_string());
417 params.insert("symbol".to_string(), symbol.to_string());
418 params.insert("intervalTime".to_string(), interval_time.to_string());
419 if let Some(l) = limit {
420 params.insert("limit".to_string(), l.to_string());
421 }
422 if let Some(st) = start_time {
423 params.insert("startTime".to_string(), st.to_string());
424 }
425 if let Some(et) = end_time {
426 params.insert("endTime".to_string(), et.to_string());
427 }
428 let response = self.get(BybitEndpoint::OpenInterest, params).await?;
429 BybitParser::parse_open_interest_list(&response)
430 }
431
432 pub async fn get_funding_rate_history(
438 &self,
439 category: &str,
440 symbol: &str,
441 start_time: Option<i64>,
442 end_time: Option<i64>,
443 limit: Option<u32>,
444 ) -> ExchangeResult<Vec<FundingRate>> {
445 let mut params = HashMap::new();
446 params.insert("category".to_string(), category.to_string());
447 params.insert("symbol".to_string(), symbol.to_string());
448 if let Some(t) = start_time {
449 params.insert("startTime".to_string(), t.to_string());
450 }
451 if let Some(t) = end_time {
452 params.insert("endTime".to_string(), t.to_string());
453 }
454 if let Some(l) = limit {
455 params.insert("limit".to_string(), l.to_string());
456 }
457 let response = self.get(BybitEndpoint::FundingRate, params).await?;
458 BybitParser::parse_funding_rates(&response)
459 }
460
461 pub async fn get_long_short_ratio(
466 &self,
467 category: &str,
468 symbol: &str,
469 period: &str,
470 limit: Option<u32>,
471 ) -> ExchangeResult<Vec<crate::core::types::LongShortRatio>> {
472 let mut params = HashMap::new();
473 params.insert("category".to_string(), category.to_string());
474 params.insert("symbol".to_string(), symbol.to_string());
475 params.insert("period".to_string(), period.to_string());
476 if let Some(l) = limit {
477 params.insert("limit".to_string(), l.to_string());
478 }
479 let response = self.get(BybitEndpoint::LongShortRatio, params).await?;
480 BybitParser::parse_long_short_ratios(&response, symbol, "account")
481 }
482
483 pub async fn get_mark_price_kline(
488 &self,
489 category: &str,
490 symbol: &str,
491 interval: &str,
492 limit: Option<u32>,
493 start: Option<i64>,
494 end: Option<i64>,
495 ) -> ExchangeResult<Vec<Kline>> {
496 let mut params = HashMap::new();
497 params.insert("category".to_string(), category.to_string());
498 params.insert("symbol".to_string(), symbol.to_string());
499 params.insert("interval".to_string(), map_kline_interval(interval).to_string());
500 if let Some(l) = limit {
501 params.insert("limit".to_string(), l.to_string());
502 }
503 if let Some(st) = start {
504 params.insert("start".to_string(), st.to_string());
505 }
506 if let Some(et) = end {
507 params.insert("end".to_string(), et.to_string());
508 }
509 let response = self.get(BybitEndpoint::MarkPriceKline, params).await?;
510 BybitParser::parse_mark_price_kline(&response)
511 }
512
513 pub async fn get_index_price_kline(
518 &self,
519 category: &str,
520 symbol: &str,
521 interval: &str,
522 limit: Option<u32>,
523 start: Option<i64>,
524 end: Option<i64>,
525 ) -> ExchangeResult<Vec<Kline>> {
526 let mut params = HashMap::new();
527 params.insert("category".to_string(), category.to_string());
528 params.insert("symbol".to_string(), symbol.to_string());
529 params.insert("interval".to_string(), map_kline_interval(interval).to_string());
530 if let Some(l) = limit {
531 params.insert("limit".to_string(), l.to_string());
532 }
533 if let Some(st) = start {
534 params.insert("start".to_string(), st.to_string());
535 }
536 if let Some(et) = end {
537 params.insert("end".to_string(), et.to_string());
538 }
539 let response = self.get(BybitEndpoint::IndexPriceKline, params).await?;
540 BybitParser::parse_mark_price_kline(&response)
541 }
542
543 pub async fn get_premium_index_kline(
548 &self,
549 category: &str,
550 symbol: &str,
551 interval: &str,
552 limit: Option<u32>,
553 start: Option<i64>,
554 end: Option<i64>,
555 ) -> ExchangeResult<Vec<Kline>> {
556 let mut params = HashMap::new();
557 params.insert("category".to_string(), category.to_string());
558 params.insert("symbol".to_string(), symbol.to_string());
559 params.insert("interval".to_string(), map_kline_interval(interval).to_string());
560 if let Some(l) = limit {
561 params.insert("limit".to_string(), l.to_string());
562 }
563 if let Some(st) = start {
564 params.insert("start".to_string(), st.to_string());
565 }
566 if let Some(et) = end {
567 params.insert("end".to_string(), et.to_string());
568 }
569 let response = self.get(BybitEndpoint::PremiumIndexKline, params).await?;
570 BybitParser::parse_mark_price_kline(&response)
571 }
572
573 pub async fn get_my_trades(
581 &self,
582 symbol: Option<&str>,
583 account_type: AccountType,
584 limit: Option<u32>,
585 start_time: Option<i64>,
586 end_time: Option<i64>,
587 ) -> ExchangeResult<Value> {
588 let mut params = HashMap::new();
589 params.insert("category".to_string(), account_type_to_category(account_type).to_string());
590 if let Some(s) = symbol {
591 params.insert("symbol".to_string(), s.to_string());
592 }
593 if let Some(l) = limit {
594 params.insert("limit".to_string(), l.to_string());
595 }
596 if let Some(st) = start_time {
597 params.insert("startTime".to_string(), st.to_string());
598 }
599 if let Some(et) = end_time {
600 params.insert("endTime".to_string(), et.to_string());
601 }
602 self.get(BybitEndpoint::MyTrades, params).await
603 }
604
605 pub async fn get_closed_pnl(
609 &self,
610 symbol: Option<&str>,
611 account_type: AccountType,
612 limit: Option<u32>,
613 start_time: Option<i64>,
614 end_time: Option<i64>,
615 ) -> ExchangeResult<Value> {
616 let mut params = HashMap::new();
617 params.insert("category".to_string(), account_type_to_category(account_type).to_string());
618 if let Some(s) = symbol {
619 params.insert("symbol".to_string(), s.to_string());
620 }
621 if let Some(l) = limit {
622 params.insert("limit".to_string(), l.to_string());
623 }
624 if let Some(st) = start_time {
625 params.insert("startTime".to_string(), st.to_string());
626 }
627 if let Some(et) = end_time {
628 params.insert("endTime".to_string(), et.to_string());
629 }
630 self.get(BybitEndpoint::ClosedPnl, params).await
631 }
632
633 pub async fn get_institutional_loan_products(&self) -> ExchangeResult<Value> {
638 self.get(BybitEndpoint::InsLoanProducts, HashMap::new()).await
639 }
640
641 pub async fn get_risk_limit(
647 &self,
648 category: &str,
649 symbol: &str,
650 ) -> ExchangeResult<Value> {
651 let mut params = HashMap::new();
652 params.insert("category".to_string(), category.to_string());
653 params.insert("symbol".to_string(), symbol.to_string());
654 self.get(BybitEndpoint::RiskLimit, params).await
655 }
656
657 pub async fn get_delivery_price(
663 &self,
664 category: &str,
665 symbol: &str,
666 limit: Option<u32>,
667 ) -> ExchangeResult<Value> {
668 let mut params = HashMap::new();
669 params.insert("category".to_string(), category.to_string());
670 params.insert("symbol".to_string(), symbol.to_string());
671 if let Some(l) = limit {
672 params.insert("limit".to_string(), l.to_string());
673 }
674 self.get(BybitEndpoint::DeliveryPrice, params).await
675 }
676}
677
678impl ExchangeIdentity for BybitConnector {
683 fn exchange_id(&self) -> ExchangeId {
684 ExchangeId::Bybit
685 }
686
687 fn metrics(&self) -> ConnectorStats {
688 let (http_requests, http_errors, last_latency_ms) = self.http.stats();
689 let (rate_used, rate_max) = if let Ok(mut limiter) = self.limiter.lock() {
690 limiter.primary_stats()
691 } else {
692 (0, 0)
693 };
694 ConnectorStats {
695 http_requests,
696 http_errors,
697 last_latency_ms,
698 rate_used,
699 rate_max,
700 rate_groups: Vec::new(),
701 ws_ping_rtt_ms: 0,
702 }
703 }
704
705 fn is_testnet(&self) -> bool {
706 self.testnet
707 }
708
709 fn supported_account_types(&self) -> Vec<AccountType> {
710 vec![
711 AccountType::Spot,
712 AccountType::Margin,
713 AccountType::FuturesCross,
714 AccountType::FuturesIsolated,
715 ]
716 }
717
718 fn exchange_type(&self) -> ExchangeType {
719 ExchangeType::Cex
720 }
721
722 fn rate_limit_capabilities(&self) -> RateLimitCapabilities {
723 BYBIT_RATE_CAPS
724 }
725
726 fn orderbook_capabilities(&self, account_type: AccountType) -> OrderbookCapabilities {
727 static SPOT_CHANNELS: &[WsBookChannel] = &[
728 WsBookChannel::snapshot("orderbook.1", 1, 10),
729 WsBookChannel::delta("orderbook.50", Some(50), Some(20)),
730 WsBookChannel::delta("orderbook.200", Some(200), Some(100)),
731 WsBookChannel::delta("orderbook.1000", Some(1000), Some(200)),
732 ];
733 static LINEAR_CHANNELS: &[WsBookChannel] = &[
734 WsBookChannel::snapshot("orderbook.1", 1, 10),
735 WsBookChannel::delta("orderbook.50", Some(50), Some(20)),
736 WsBookChannel::delta("orderbook.200", Some(200), Some(100)),
737 WsBookChannel::delta("orderbook.1000", Some(1000), Some(200)),
738 ];
739 static OPTION_CHANNELS: &[WsBookChannel] = &[
740 WsBookChannel::delta("orderbook.25", Some(25), Some(20)),
741 WsBookChannel::delta("orderbook.100", Some(100), Some(100)),
742 ];
743 match account_type {
744 AccountType::Options => OrderbookCapabilities {
745 ws_depths: &[25, 100],
746 ws_default_depth: Some(25),
747 rest_max_depth: Some(25),
748 rest_depth_values: &[],
749 supports_snapshot: true,
750 supports_delta: true,
751 update_speeds_ms: &[20, 100],
752 default_speed_ms: Some(20),
753 ws_channels: OPTION_CHANNELS,
754 checksum: None,
755 has_sequence: true,
756 has_prev_sequence: false,
757 supports_aggregation: false,
758 aggregation_levels: &[],
759 },
760 AccountType::Spot => OrderbookCapabilities {
761 ws_depths: &[1, 50, 200, 1000],
762 ws_default_depth: Some(50),
763 rest_max_depth: Some(200),
764 rest_depth_values: &[],
765 supports_snapshot: true,
766 supports_delta: true,
767 update_speeds_ms: &[10, 20, 100, 200],
768 default_speed_ms: Some(20),
769 ws_channels: SPOT_CHANNELS,
770 checksum: None,
771 has_sequence: true,
772 has_prev_sequence: false,
773 supports_aggregation: false,
774 aggregation_levels: &[],
775 },
776 _ => OrderbookCapabilities {
777 ws_depths: &[1, 50, 200, 1000],
778 ws_default_depth: Some(50),
779 rest_max_depth: Some(500),
780 rest_depth_values: &[],
781 supports_snapshot: true,
782 supports_delta: true,
783 update_speeds_ms: &[10, 20, 100, 200],
784 default_speed_ms: Some(20),
785 ws_channels: LINEAR_CHANNELS,
786 checksum: None,
787 has_sequence: true,
788 has_prev_sequence: false,
789 supports_aggregation: false,
790 aggregation_levels: &[],
791 },
792 }
793 }
794}
795
796#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
801#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
802impl MarketData for BybitConnector {
803 async fn get_price(
804 &self,
805 symbol: SymbolInput<'_>,
806 account_type: AccountType,
807 ) -> ExchangeResult<Price> {
808 let symbol = symbol.resolve(ExchangeId::Bybit, account_type)?;
809 let mut params = HashMap::new();
810 params.insert("category".to_string(), account_type_to_category(account_type).to_string());
811 params.insert("symbol".to_string(), symbol.to_string());
812
813 let response = self.get(BybitEndpoint::Ticker, params).await?;
814 let ticker = BybitParser::parse_ticker(&response)?;
815 Ok(ticker.last_price)
816 }
817
818 async fn get_orderbook(
819 &self,
820 symbol: SymbolInput<'_>,
821 depth: Option<u16>,
822 account_type: AccountType,
823 ) -> ExchangeResult<OrderBook> {
824 let symbol = symbol.resolve(ExchangeId::Bybit, account_type)?;
825 let mut params = HashMap::new();
826 params.insert("category".to_string(), account_type_to_category(account_type).to_string());
827 params.insert("symbol".to_string(), symbol.to_string());
828
829 if let Some(d) = depth {
830 params.insert("limit".to_string(), d.to_string());
831 }
832
833 let response = self.get(BybitEndpoint::Orderbook, params).await?;
834 BybitParser::parse_orderbook(&response)
835 }
836
837 async fn get_klines(
838 &self,
839 symbol: SymbolInput<'_>,
840 interval: &str,
841 limit: Option<u16>,
842 account_type: AccountType,
843 end_time: Option<i64>,
844 ) -> ExchangeResult<Vec<Kline>> {
845 let symbol = symbol.resolve(ExchangeId::Bybit, account_type)?;
846 let mut params = HashMap::new();
847 params.insert("category".to_string(), account_type_to_category(account_type).to_string());
848 params.insert("symbol".to_string(), symbol.to_string());
849 params.insert("interval".to_string(), map_kline_interval(interval).to_string());
850
851 if let Some(l) = limit {
852 params.insert("limit".to_string(), l.min(1000).to_string());
853 }
854
855 if let Some(et) = end_time {
856 params.insert("end".to_string(), et.to_string());
857 }
858
859 let response = self.get(BybitEndpoint::Klines, params).await?;
860 BybitParser::parse_klines(&response)
861 }
862
863 async fn get_ticker(
864 &self,
865 symbol: SymbolInput<'_>,
866 account_type: AccountType,
867 ) -> ExchangeResult<Ticker> {
868 let symbol = symbol.resolve(ExchangeId::Bybit, account_type)?;
869 let mut params = HashMap::new();
870 params.insert("category".to_string(), account_type_to_category(account_type).to_string());
871 params.insert("symbol".to_string(), symbol.to_string());
872
873 let response = self.get(BybitEndpoint::Ticker, params).await?;
874 BybitParser::parse_ticker(&response)
875 }
876
877 async fn ping(&self) -> ExchangeResult<()> {
878 let response = self.get(BybitEndpoint::ServerTime, HashMap::new()).await?;
879 self.check_response(&response)
880 }
881
882 async fn get_exchange_info(&self, account_type: AccountType) -> ExchangeResult<Vec<crate::core::types::SymbolInfo>> {
883 let response = self.get_symbols(account_type).await?;
884 let symbols = BybitParser::parse_exchange_info(&response, account_type)?;
885 self.precision.load_from_symbols(&symbols);
886 Ok(symbols)
887 }
888
889 fn market_data_capabilities(&self, _account_type: AccountType) -> MarketDataCapabilities {
890 MarketDataCapabilities {
894 has_ping: true,
895 has_price: true,
896 has_ticker: true,
897 has_orderbook: true,
898 has_klines: true,
899 has_exchange_info: true,
900 has_recent_trades: false,
902 has_ws_klines: true,
903 has_ws_trades: true,
904 has_ws_orderbook: true,
905 has_ws_ticker: true,
906 supported_intervals: &[
908 "1m", "3m", "5m", "15m", "30m",
909 "1h", "2h", "4h", "6h", "12h",
910 "1d", "1w", "1M",
911 ],
912 max_kline_limit: Some(1000),
914 }
915 }
916}
917
918#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
923#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
924impl Trading for BybitConnector {
925 async fn place_order(&self, req: OrderRequest) -> ExchangeResult<PlaceOrderResponse> {
926 let symbol = req.symbol.clone();
927 let side = req.side;
928 let quantity = req.quantity;
929 let account_type = req.account_type;
930 let symbol_str = format_symbol(&symbol, account_type);
931
932 match req.order_type {
933 OrderType::Market => {
934 let order_link_id = format!("cc_{}", crate::core::timestamp_millis());
935
936 let body = json!({
937 "category": account_type_to_category(account_type),
938 "symbol": format_symbol(&symbol, account_type),
939 "side": match side {
940 OrderSide::Buy => "Buy",
941 OrderSide::Sell => "Sell",
942 },
943 "orderType": "Market",
944 "qty": self.precision.qty(&symbol_str, quantity),
945 "orderLinkId": order_link_id,
946 });
947
948 let response = self.post(BybitEndpoint::PlaceOrder, body).await?;
949
950 let result = response.get("result")
952 .ok_or_else(|| ExchangeError::Parse("Missing result".to_string()))?;
953
954 let order_id = result.get("orderId")
955 .and_then(|id| id.as_str())
956 .ok_or_else(|| ExchangeError::Parse("Missing orderId".to_string()))?
957 .to_string();
958
959 Ok(PlaceOrderResponse::Simple(Order {
961 id: order_id,
962 client_order_id: Some(order_link_id),
963 symbol: Some(symbol.to_string()),
964 side,
965 order_type: OrderType::Market,
966 status: crate::core::OrderStatus::New,
967 price: None,
968 stop_price: None,
969 quantity,
970 filled_quantity: 0.0,
971 average_price: None,
972 commission: None,
973 commission_asset: None,
974 created_at: crate::core::timestamp_millis() as i64,
975 updated_at: None,
976 time_in_force: crate::core::TimeInForce::Gtc,
977 }))
978 }
979 OrderType::Limit { price } => {
980 let order_link_id = req.client_order_id.clone()
981 .unwrap_or_else(|| format!("cc_{}", crate::core::timestamp_millis()));
982 let tif = match req.time_in_force {
983 crate::core::TimeInForce::Gtc => "GTC",
984 crate::core::TimeInForce::Ioc => "IOC",
985 crate::core::TimeInForce::Fok => "FOK",
986 crate::core::TimeInForce::PostOnly => "PostOnly",
987 _ => "GTC",
988 };
989
990 let mut body = json!({
991 "category": account_type_to_category(account_type),
992 "symbol": format_symbol(&symbol, account_type),
993 "side": match side {
994 OrderSide::Buy => "Buy",
995 OrderSide::Sell => "Sell",
996 },
997 "orderType": "Limit",
998 "qty": self.precision.qty(&symbol_str, quantity),
999 "price": self.precision.price(&symbol_str, price),
1000 "timeInForce": tif,
1001 "orderLinkId": order_link_id,
1002 });
1003 if req.reduce_only {
1004 body["reduceOnly"] = json!(true);
1005 }
1006
1007 let response = self.post(BybitEndpoint::PlaceOrder, body).await?;
1008
1009 let result = response.get("result")
1010 .ok_or_else(|| ExchangeError::Parse("Missing result".to_string()))?;
1011
1012 let order_id = result.get("orderId")
1013 .and_then(|id| id.as_str())
1014 .ok_or_else(|| ExchangeError::Parse("Missing orderId".to_string()))?
1015 .to_string();
1016
1017 Ok(PlaceOrderResponse::Simple(Order {
1018 id: order_id,
1019 client_order_id: Some(order_link_id),
1020 symbol: Some(symbol.to_string()),
1021 side,
1022 order_type: OrderType::Limit { price },
1023 status: crate::core::OrderStatus::New,
1024 price: Some(price),
1025 stop_price: None,
1026 quantity,
1027 filled_quantity: 0.0,
1028 average_price: None,
1029 commission: None,
1030 commission_asset: None,
1031 created_at: crate::core::timestamp_millis() as i64,
1032 updated_at: None,
1033 time_in_force: req.time_in_force,
1034 }))
1035 }
1036 OrderType::StopMarket { stop_price } => {
1037 let order_link_id = req.client_order_id.clone()
1038 .unwrap_or_else(|| format!("cc_{}", crate::core::timestamp_millis()));
1039
1040 let mut body = json!({
1041 "category": account_type_to_category(account_type),
1042 "symbol": format_symbol(&symbol, account_type),
1043 "side": match side {
1044 OrderSide::Buy => "Buy",
1045 OrderSide::Sell => "Sell",
1046 },
1047 "orderType": "Market",
1048 "qty": self.precision.qty(&symbol_str, quantity),
1049 "triggerPrice": self.precision.price(&symbol_str, stop_price),
1050 "triggerBy": "MarkPrice",
1051 "orderLinkId": order_link_id,
1052 });
1053 if req.reduce_only {
1054 body["reduceOnly"] = json!(true);
1055 }
1056
1057 let response = self.post(BybitEndpoint::PlaceOrder, body).await?;
1058 let result = response.get("result")
1059 .ok_or_else(|| ExchangeError::Parse("Missing result".to_string()))?;
1060 let order_id = result.get("orderId")
1061 .and_then(|id| id.as_str())
1062 .ok_or_else(|| ExchangeError::Parse("Missing orderId".to_string()))?
1063 .to_string();
1064
1065 Ok(PlaceOrderResponse::Simple(Order {
1066 id: order_id,
1067 client_order_id: Some(order_link_id),
1068 symbol: Some(symbol.to_string()),
1069 side,
1070 order_type: OrderType::StopMarket { stop_price },
1071 status: crate::core::OrderStatus::New,
1072 price: None,
1073 stop_price: Some(stop_price),
1074 quantity,
1075 filled_quantity: 0.0,
1076 average_price: None,
1077 commission: None,
1078 commission_asset: None,
1079 created_at: crate::core::timestamp_millis() as i64,
1080 updated_at: None,
1081 time_in_force: crate::core::TimeInForce::Gtc,
1082 }))
1083 }
1084 OrderType::StopLimit { stop_price, limit_price } => {
1085 let order_link_id = req.client_order_id.clone()
1086 .unwrap_or_else(|| format!("cc_{}", crate::core::timestamp_millis()));
1087
1088 let mut body = json!({
1089 "category": account_type_to_category(account_type),
1090 "symbol": format_symbol(&symbol, account_type),
1091 "side": match side {
1092 OrderSide::Buy => "Buy",
1093 OrderSide::Sell => "Sell",
1094 },
1095 "orderType": "Limit",
1096 "qty": self.precision.qty(&symbol_str, quantity),
1097 "price": self.precision.price(&symbol_str, limit_price),
1098 "triggerPrice": self.precision.price(&symbol_str, stop_price),
1099 "triggerBy": "MarkPrice",
1100 "orderLinkId": order_link_id,
1101 });
1102 if req.reduce_only {
1103 body["reduceOnly"] = json!(true);
1104 }
1105
1106 let response = self.post(BybitEndpoint::PlaceOrder, body).await?;
1107 let result = response.get("result")
1108 .ok_or_else(|| ExchangeError::Parse("Missing result".to_string()))?;
1109 let order_id = result.get("orderId")
1110 .and_then(|id| id.as_str())
1111 .ok_or_else(|| ExchangeError::Parse("Missing orderId".to_string()))?
1112 .to_string();
1113
1114 Ok(PlaceOrderResponse::Simple(Order {
1115 id: order_id,
1116 client_order_id: Some(order_link_id),
1117 symbol: Some(symbol.to_string()),
1118 side,
1119 order_type: OrderType::StopLimit { stop_price, limit_price },
1120 status: crate::core::OrderStatus::New,
1121 price: Some(limit_price),
1122 stop_price: Some(stop_price),
1123 quantity,
1124 filled_quantity: 0.0,
1125 average_price: None,
1126 commission: None,
1127 commission_asset: None,
1128 created_at: crate::core::timestamp_millis() as i64,
1129 updated_at: None,
1130 time_in_force: crate::core::TimeInForce::Gtc,
1131 }))
1132 }
1133 OrderType::TrailingStop { callback_rate, activation_price } => {
1134 match account_type {
1136 AccountType::Spot | AccountType::Margin => {
1137 return Err(ExchangeError::UnsupportedOperation(
1138 "TrailingStop not supported for Spot/Margin on Bybit".to_string()
1139 ));
1140 }
1141 _ => {}
1142 }
1143
1144 let order_link_id = req.client_order_id.clone()
1145 .unwrap_or_else(|| format!("cc_{}", crate::core::timestamp_millis()));
1146
1147 let mut body = json!({
1148 "category": "linear",
1149 "symbol": format_symbol(&symbol, account_type),
1150 "side": match side {
1151 OrderSide::Buy => "Buy",
1152 OrderSide::Sell => "Sell",
1153 },
1154 "orderType": "Market",
1155 "qty": self.precision.qty(&symbol_str, quantity),
1156 "trailingStop": callback_rate.to_string(),
1157 "orderLinkId": order_link_id,
1158 });
1159 if let Some(ap) = activation_price {
1160 body["activePrice"] = json!(self.precision.price(&symbol_str, ap));
1161 }
1162 if req.reduce_only {
1163 body["reduceOnly"] = json!(true);
1164 }
1165
1166 let response = self.post(BybitEndpoint::PlaceOrder, body).await?;
1167 let result = response.get("result")
1168 .ok_or_else(|| ExchangeError::Parse("Missing result".to_string()))?;
1169 let order_id = result.get("orderId")
1170 .and_then(|id| id.as_str())
1171 .ok_or_else(|| ExchangeError::Parse("Missing orderId".to_string()))?
1172 .to_string();
1173
1174 Ok(PlaceOrderResponse::Simple(Order {
1175 id: order_id,
1176 client_order_id: Some(order_link_id),
1177 symbol: Some(symbol.to_string()),
1178 side,
1179 order_type: OrderType::TrailingStop { callback_rate, activation_price },
1180 status: crate::core::OrderStatus::New,
1181 price: None,
1182 stop_price: activation_price,
1183 quantity,
1184 filled_quantity: 0.0,
1185 average_price: None,
1186 commission: None,
1187 commission_asset: None,
1188 created_at: crate::core::timestamp_millis() as i64,
1189 updated_at: None,
1190 time_in_force: crate::core::TimeInForce::Gtc,
1191 }))
1192 }
1193 OrderType::PostOnly { price } => {
1194 let order_link_id = req.client_order_id.clone()
1195 .unwrap_or_else(|| format!("cc_{}", crate::core::timestamp_millis()));
1196
1197 let body = json!({
1198 "category": account_type_to_category(account_type),
1199 "symbol": format_symbol(&symbol, account_type),
1200 "side": match side {
1201 OrderSide::Buy => "Buy",
1202 OrderSide::Sell => "Sell",
1203 },
1204 "orderType": "Limit",
1205 "qty": self.precision.qty(&symbol_str, quantity),
1206 "price": self.precision.price(&symbol_str, price),
1207 "timeInForce": "PostOnly",
1208 "orderLinkId": order_link_id,
1209 });
1210
1211 let response = self.post(BybitEndpoint::PlaceOrder, body).await?;
1212 let result = response.get("result")
1213 .ok_or_else(|| ExchangeError::Parse("Missing result".to_string()))?;
1214 let order_id = result.get("orderId")
1215 .and_then(|id| id.as_str())
1216 .ok_or_else(|| ExchangeError::Parse("Missing orderId".to_string()))?
1217 .to_string();
1218
1219 Ok(PlaceOrderResponse::Simple(Order {
1220 id: order_id,
1221 client_order_id: Some(order_link_id),
1222 symbol: Some(symbol.to_string()),
1223 side,
1224 order_type: OrderType::PostOnly { price },
1225 status: crate::core::OrderStatus::New,
1226 price: Some(price),
1227 stop_price: None,
1228 quantity,
1229 filled_quantity: 0.0,
1230 average_price: None,
1231 commission: None,
1232 commission_asset: None,
1233 created_at: crate::core::timestamp_millis() as i64,
1234 updated_at: None,
1235 time_in_force: crate::core::TimeInForce::PostOnly,
1236 }))
1237 }
1238 OrderType::Ioc { price } => {
1239 let order_link_id = req.client_order_id.clone()
1240 .unwrap_or_else(|| format!("cc_{}", crate::core::timestamp_millis()));
1241
1242 let body = json!({
1243 "category": account_type_to_category(account_type),
1244 "symbol": format_symbol(&symbol, account_type),
1245 "side": match side {
1246 OrderSide::Buy => "Buy",
1247 OrderSide::Sell => "Sell",
1248 },
1249 "orderType": if price.is_some() { "Limit" } else { "Market" },
1250 "qty": self.precision.qty(&symbol_str, quantity),
1251 "price": price.map(|p| self.precision.price(&symbol_str, p)).unwrap_or_default(),
1252 "timeInForce": "IOC",
1253 "orderLinkId": order_link_id,
1254 });
1255
1256 let response = self.post(BybitEndpoint::PlaceOrder, body).await?;
1257 let result = response.get("result")
1258 .ok_or_else(|| ExchangeError::Parse("Missing result".to_string()))?;
1259 let order_id = result.get("orderId")
1260 .and_then(|id| id.as_str())
1261 .ok_or_else(|| ExchangeError::Parse("Missing orderId".to_string()))?
1262 .to_string();
1263
1264 Ok(PlaceOrderResponse::Simple(Order {
1265 id: order_id,
1266 client_order_id: Some(order_link_id),
1267 symbol: Some(symbol.to_string()),
1268 side,
1269 order_type: OrderType::Ioc { price },
1270 status: crate::core::OrderStatus::New,
1271 price,
1272 stop_price: None,
1273 quantity,
1274 filled_quantity: 0.0,
1275 average_price: None,
1276 commission: None,
1277 commission_asset: None,
1278 created_at: crate::core::timestamp_millis() as i64,
1279 updated_at: None,
1280 time_in_force: crate::core::TimeInForce::Ioc,
1281 }))
1282 }
1283 OrderType::Fok { price } => {
1284 let order_link_id = req.client_order_id.clone()
1285 .unwrap_or_else(|| format!("cc_{}", crate::core::timestamp_millis()));
1286
1287 let body = json!({
1288 "category": account_type_to_category(account_type),
1289 "symbol": format_symbol(&symbol, account_type),
1290 "side": match side {
1291 OrderSide::Buy => "Buy",
1292 OrderSide::Sell => "Sell",
1293 },
1294 "orderType": "Limit",
1295 "qty": self.precision.qty(&symbol_str, quantity),
1296 "price": self.precision.price(&symbol_str, price),
1297 "timeInForce": "FOK",
1298 "orderLinkId": order_link_id,
1299 });
1300
1301 let response = self.post(BybitEndpoint::PlaceOrder, body).await?;
1302 let result = response.get("result")
1303 .ok_or_else(|| ExchangeError::Parse("Missing result".to_string()))?;
1304 let order_id = result.get("orderId")
1305 .and_then(|id| id.as_str())
1306 .ok_or_else(|| ExchangeError::Parse("Missing orderId".to_string()))?
1307 .to_string();
1308
1309 Ok(PlaceOrderResponse::Simple(Order {
1310 id: order_id,
1311 client_order_id: Some(order_link_id),
1312 symbol: Some(symbol.to_string()),
1313 side,
1314 order_type: OrderType::Fok { price },
1315 status: crate::core::OrderStatus::New,
1316 price: Some(price),
1317 stop_price: None,
1318 quantity,
1319 filled_quantity: 0.0,
1320 average_price: None,
1321 commission: None,
1322 commission_asset: None,
1323 created_at: crate::core::timestamp_millis() as i64,
1324 updated_at: None,
1325 time_in_force: crate::core::TimeInForce::Fok,
1326 }))
1327 }
1328 OrderType::Gtd { .. } => {
1329 Err(ExchangeError::UnsupportedOperation(
1333 "GTD orders are not supported on Bybit (not in TimeInForce enum)".to_string()
1334 ))
1335 }
1336 OrderType::ReduceOnly { price } => {
1337 match account_type {
1338 AccountType::Spot | AccountType::Margin => {
1339 return Err(ExchangeError::UnsupportedOperation(
1340 "ReduceOnly not supported for Spot/Margin".to_string()
1341 ));
1342 }
1343 _ => {}
1344 }
1345
1346 let order_link_id = req.client_order_id.clone()
1347 .unwrap_or_else(|| format!("cc_{}", crate::core::timestamp_millis()));
1348
1349 let body = json!({
1350 "category": "linear",
1351 "symbol": format_symbol(&symbol, account_type),
1352 "side": match side {
1353 OrderSide::Buy => "Buy",
1354 OrderSide::Sell => "Sell",
1355 },
1356 "orderType": if price.is_some() { "Limit" } else { "Market" },
1357 "qty": self.precision.qty(&symbol_str, quantity),
1358 "price": price.map(|p| self.precision.price(&symbol_str, p)).unwrap_or_default(),
1359 "reduceOnly": true,
1360 "orderLinkId": order_link_id,
1361 });
1362
1363 let response = self.post(BybitEndpoint::PlaceOrder, body).await?;
1364 let result = response.get("result")
1365 .ok_or_else(|| ExchangeError::Parse("Missing result".to_string()))?;
1366 let order_id = result.get("orderId")
1367 .and_then(|id| id.as_str())
1368 .ok_or_else(|| ExchangeError::Parse("Missing orderId".to_string()))?
1369 .to_string();
1370
1371 Ok(PlaceOrderResponse::Simple(Order {
1372 id: order_id,
1373 client_order_id: Some(order_link_id),
1374 symbol: Some(symbol.to_string()),
1375 side,
1376 order_type: OrderType::ReduceOnly { price },
1377 status: crate::core::OrderStatus::New,
1378 price,
1379 stop_price: None,
1380 quantity,
1381 filled_quantity: 0.0,
1382 average_price: None,
1383 commission: None,
1384 commission_asset: None,
1385 created_at: crate::core::timestamp_millis() as i64,
1386 updated_at: None,
1387 time_in_force: crate::core::TimeInForce::Gtc,
1388 }))
1389 }
1390 OrderType::Iceberg { price, display_quantity } => {
1391 let order_link_id = req.client_order_id.clone()
1395 .unwrap_or_else(|| format!("cc_{}", crate::core::timestamp_millis()));
1396
1397 let body = json!({
1398 "category": account_type_to_category(account_type),
1399 "symbol": format_symbol(&symbol, account_type),
1400 "side": match side {
1401 OrderSide::Buy => "Buy",
1402 OrderSide::Sell => "Sell",
1403 },
1404 "orderType": "Limit",
1405 "qty": self.precision.qty(&symbol_str, quantity),
1406 "price": self.precision.price(&symbol_str, price),
1407 "timeInForce": "GTC",
1408 "peakOrderQty": self.precision.qty(&symbol_str, display_quantity),
1409 "orderLinkId": order_link_id,
1410 });
1411
1412 let response = self.post(BybitEndpoint::PlaceOrder, body).await?;
1413 let result = response.get("result")
1414 .ok_or_else(|| ExchangeError::Parse("Missing result".to_string()))?;
1415 let order_id = result.get("orderId")
1416 .and_then(|id| id.as_str())
1417 .ok_or_else(|| ExchangeError::Parse("Missing orderId".to_string()))?
1418 .to_string();
1419
1420 Ok(PlaceOrderResponse::Simple(Order {
1421 id: order_id,
1422 client_order_id: Some(order_link_id),
1423 symbol: Some(symbol.to_string()),
1424 side,
1425 order_type: OrderType::Iceberg { price, display_quantity },
1426 status: crate::core::OrderStatus::New,
1427 price: Some(price),
1428 stop_price: None,
1429 quantity,
1430 filled_quantity: 0.0,
1431 average_price: None,
1432 commission: None,
1433 commission_asset: None,
1434 created_at: crate::core::timestamp_millis() as i64,
1435 updated_at: None,
1436 time_in_force: crate::core::TimeInForce::Gtc,
1437 }))
1438 }
1439 OrderType::Oco { .. } => Err(ExchangeError::UnsupportedOperation(
1444 "OCO orders are not available via Bybit API (UI only)".to_string()
1445 )),
1446 OrderType::Bracket { .. } => Err(ExchangeError::UnsupportedOperation(
1447 "Bracket orders are not supported on Bybit API (no native bracket type)".to_string()
1448 )),
1449 OrderType::Twap { .. } => Err(ExchangeError::UnsupportedOperation(
1450 "TWAP orders are not available via Bybit API (UI-only strategy feature)".to_string()
1451 )),
1452 _ => Err(ExchangeError::UnsupportedOperation(
1453 "This order type is not supported by Bybit".to_string()
1454 )),
1455 }
1456 }
1457
1458 async fn get_order_history(
1459 &self,
1460 filter: OrderHistoryFilter,
1461 account_type: AccountType,
1462 ) -> ExchangeResult<Vec<Order>> {
1463 let mut params = HashMap::new();
1464 params.insert("category".to_string(), account_type_to_category(account_type).to_string());
1465
1466 if let Some(ref s) = filter.symbol {
1467 params.insert("symbol".to_string(), format_symbol(s, account_type));
1468 }
1469 if let Some(st) = filter.start_time {
1470 params.insert("startTime".to_string(), st.to_string());
1471 }
1472 if let Some(et) = filter.end_time {
1473 params.insert("endTime".to_string(), et.to_string());
1474 }
1475 if let Some(lim) = filter.limit {
1476 params.insert("limit".to_string(), lim.min(50).to_string());
1477 }
1478
1479 let response = self.get(BybitEndpoint::OrderHistory, params).await?;
1480
1481 let result = response.get("result")
1482 .ok_or_else(|| ExchangeError::Parse("Missing result".to_string()))?;
1483 let list = result.get("list")
1484 .and_then(|l| l.as_array())
1485 .ok_or_else(|| ExchangeError::Parse("Missing list".to_string()))?;
1486
1487 let mut orders = Vec::new();
1488 for order_data in list {
1489 let wrapper = serde_json::json!({
1490 "retCode": 0,
1491 "retMsg": "OK",
1492 "result": order_data,
1493 });
1494 if let Ok(order) = BybitParser::parse_order(&wrapper) {
1495 orders.push(order);
1496 }
1497 }
1498
1499 Ok(orders)
1500 }
1501
1502 async fn cancel_order(&self, req: CancelRequest) -> ExchangeResult<Order> {
1503 match req.scope {
1504 CancelScope::Single { ref order_id } => {
1505 let symbol = req.symbol.as_ref()
1506 .ok_or_else(|| ExchangeError::InvalidRequest("Symbol required for cancel".into()))?
1507 .clone();
1508 let account_type = req.account_type;
1509
1510 let body = json!({
1511 "category": account_type_to_category(account_type),
1512 "symbol": format_symbol(&symbol, account_type),
1513 "orderId": order_id,
1514 });
1515
1516 let response = self.post(BybitEndpoint::CancelOrder, body).await?;
1517 self.check_response(&response)?;
1518
1519 Ok(Order {
1520 id: order_id.to_string(),
1521 client_order_id: None,
1522 symbol: Some(symbol.to_string()),
1523 side: OrderSide::Buy,
1524 order_type: OrderType::Limit { price: 0.0 },
1525 status: crate::core::OrderStatus::Canceled,
1526 price: None,
1527 stop_price: None,
1528 quantity: 0.0,
1529 filled_quantity: 0.0,
1530 average_price: None,
1531 commission: None,
1532 commission_asset: None,
1533 created_at: 0,
1534 updated_at: Some(crate::core::timestamp_millis() as i64),
1535 time_in_force: crate::core::TimeInForce::Gtc,
1536 })
1537 }
1538 CancelScope::All { ref symbol } => {
1539 let sym = symbol.as_ref()
1540 .ok_or_else(|| ExchangeError::InvalidRequest("Symbol required for cancel-all on Bybit".into()))?;
1541 let account_type = req.account_type;
1542
1543 let body = json!({
1544 "category": account_type_to_category(account_type),
1545 "symbol": format_symbol(sym, account_type),
1546 });
1547
1548 let response = self.post(BybitEndpoint::CancelAllOrders, body).await?;
1549 self.check_response(&response)?;
1550
1551 Ok(Order {
1553 id: "cancel-all".to_string(),
1554 client_order_id: None,
1555 symbol: Some(sym.to_string()),
1556 side: OrderSide::Buy,
1557 order_type: OrderType::Market,
1558 status: crate::core::OrderStatus::Canceled,
1559 price: None,
1560 stop_price: None,
1561 quantity: 0.0,
1562 filled_quantity: 0.0,
1563 average_price: None,
1564 commission: None,
1565 commission_asset: None,
1566 created_at: 0,
1567 updated_at: Some(crate::core::timestamp_millis() as i64),
1568 time_in_force: crate::core::TimeInForce::Gtc,
1569 })
1570 }
1571 CancelScope::BySymbol { ref symbol } => {
1572 let account_type = req.account_type;
1573
1574 let body = json!({
1575 "category": account_type_to_category(account_type),
1576 "symbol": format_symbol(symbol, account_type),
1577 });
1578
1579 let response = self.post(BybitEndpoint::CancelAllOrders, body).await?;
1580 self.check_response(&response)?;
1581
1582 Ok(Order {
1583 id: "cancel-by-symbol".to_string(),
1584 client_order_id: None,
1585 symbol: Some(symbol.to_string()),
1586 side: OrderSide::Buy,
1587 order_type: OrderType::Market,
1588 status: crate::core::OrderStatus::Canceled,
1589 price: None,
1590 stop_price: None,
1591 quantity: 0.0,
1592 filled_quantity: 0.0,
1593 average_price: None,
1594 commission: None,
1595 commission_asset: None,
1596 created_at: 0,
1597 updated_at: Some(crate::core::timestamp_millis() as i64),
1598 time_in_force: crate::core::TimeInForce::Gtc,
1599 })
1600 }
1601 CancelScope::Batch { ref order_ids } => {
1602 let _ = order_ids;
1605 Err(ExchangeError::UnsupportedOperation(
1606 "Batch cancel not natively supported on Bybit V5 (no atomic batch-cancel endpoint)".to_string()
1607 ))
1608 }
1609 _ => Err(ExchangeError::UnsupportedOperation(
1610 "This cancel scope is not supported by Bybit".to_string()
1611 )),
1612 }
1613 }
1614
1615 async fn get_order(
1616 &self,
1617 symbol: &str,
1618 order_id: &str,
1619 account_type: AccountType,
1620 ) -> ExchangeResult<Order> {
1621 let symbol_parts: Vec<&str> = symbol.split('/').collect();
1623 let symbol = if symbol_parts.len() == 2 {
1624 crate::core::Symbol::new(symbol_parts[0], symbol_parts[1])
1625 } else {
1626 crate::core::Symbol { base: symbol.to_string(), quote: String::new(), raw: Some(symbol.to_string()) }
1627 };
1628
1629 let mut params = HashMap::new();
1630 params.insert("category".to_string(), account_type_to_category(account_type).to_string());
1631 params.insert("symbol".to_string(), format_symbol(&symbol, account_type));
1632 params.insert("orderId".to_string(), order_id.to_string());
1633
1634 let response = self.get(BybitEndpoint::OrderStatus, params).await?;
1635 BybitParser::parse_order(&response)
1636
1637 }
1638
1639 async fn get_open_orders(
1640 &self,
1641 symbol: Option<&str>,
1642 account_type: AccountType,
1643 ) -> ExchangeResult<Vec<Order>> {
1644 let symbol_str = symbol;
1646 let symbol: Option<crate::core::Symbol> = symbol_str.map(|s| {
1647 let parts: Vec<&str> = s.split('/').collect();
1648 if parts.len() == 2 {
1649 crate::core::Symbol::new(parts[0], parts[1])
1650 } else {
1651 crate::core::Symbol { base: s.to_string(), quote: String::new(), raw: Some(s.to_string()) }
1652 }
1653 });
1654
1655 let mut params = HashMap::new();
1656 params.insert("category".to_string(), account_type_to_category(account_type).to_string());
1657
1658 if let Some(s) = symbol {
1659 params.insert("symbol".to_string(), format_symbol(&s, account_type));
1660 }
1661
1662 let response = self.get(BybitEndpoint::OpenOrders, params).await?;
1663
1664 let result = response.get("result")
1666 .ok_or_else(|| ExchangeError::Parse("Missing result".to_string()))?;
1667
1668 let list = result.get("list")
1669 .and_then(|l| l.as_array())
1670 .ok_or_else(|| ExchangeError::Parse("Missing list".to_string()))?;
1671
1672 let mut orders = Vec::new();
1673 for order_data in list {
1674 let wrapper = json!({
1676 "retCode": 0,
1677 "retMsg": "OK",
1678 "result": order_data,
1679 });
1680
1681 if let Ok(order) = BybitParser::parse_order(&wrapper) {
1682 orders.push(order);
1683 }
1684 }
1685
1686 Ok(orders)
1687
1688 }
1689
1690 async fn get_user_trades(
1691 &self,
1692 filter: UserTradeFilter,
1693 account_type: AccountType,
1694 ) -> ExchangeResult<Vec<UserTrade>> {
1695 let mut params = HashMap::new();
1696 params.insert("category".to_string(), account_type_to_category(account_type).to_string());
1697
1698 if let Some(ref s) = filter.symbol {
1699 params.insert("symbol".to_string(), s.clone());
1700 }
1701 if let Some(ref oid) = filter.order_id {
1702 params.insert("orderId".to_string(), oid.clone());
1703 }
1704 if let Some(st) = filter.start_time {
1705 params.insert("startTime".to_string(), st.to_string());
1706 }
1707 if let Some(et) = filter.end_time {
1708 params.insert("endTime".to_string(), et.to_string());
1709 }
1710 if let Some(lim) = filter.limit {
1711 params.insert("limit".to_string(), lim.min(100).to_string());
1712 }
1713
1714 let response = self.get(BybitEndpoint::MyTrades, params).await?;
1715
1716 let result = response.get("result")
1717 .ok_or_else(|| ExchangeError::Parse("Missing result".to_string()))?;
1718 let list = result.get("list")
1719 .and_then(|l| l.as_array())
1720 .ok_or_else(|| ExchangeError::Parse("Missing list".to_string()))?;
1721
1722 let trades = list
1723 .iter()
1724 .filter_map(|item| BybitParser::parse_user_trade(item).ok())
1725 .collect();
1726
1727 Ok(trades)
1728 }
1729
1730 fn trading_capabilities(&self, account_type: AccountType) -> TradingCapabilities {
1731 let is_futures = !matches!(account_type, AccountType::Spot | AccountType::Margin);
1732 TradingCapabilities {
1733 has_market_order: true,
1734 has_limit_order: true,
1735 has_stop_market: true,
1736 has_stop_limit: true,
1737 has_trailing_stop: is_futures,
1739 has_bracket: false,
1741 has_oco: false,
1743 has_amend: true,
1745 has_batch: true,
1747 max_batch_size: Some(10),
1749 has_cancel_all: true,
1751 has_user_trades: true,
1752 has_order_history: true,
1753 }
1754 }
1755}
1756
1757#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
1762#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
1763impl Account for BybitConnector {
1764 async fn get_balance(&self, query: BalanceQuery) -> ExchangeResult<Vec<Balance>> {
1765 let _asset = query.asset.clone();
1766 let account_type = query.account_type;
1767
1768 let mut params = HashMap::new();
1769 params.insert("accountType".to_string(), match account_type {
1770 AccountType::Spot | AccountType::Margin => "UNIFIED",
1771 AccountType::FuturesCross | AccountType::FuturesIsolated => "CONTRACT",
1772 _ => "UNIFIED",
1773 }.to_string());
1774
1775 let response = self.get(BybitEndpoint::Balance, params).await?;
1776 BybitParser::parse_balance(&response)
1777
1778 }
1779
1780 async fn get_account_info(&self, account_type: AccountType) -> ExchangeResult<AccountInfo> {
1781 let response = self.get(BybitEndpoint::AccountInfo, HashMap::new()).await?;
1782
1783 let balances = self.get_balance(BalanceQuery { asset: None, account_type }).await?;
1785
1786 let result = response.get("result")
1788 .ok_or_else(|| ExchangeError::Parse("Missing result".to_string()))?;
1789
1790 let can_trade = result.get("unifiedMarginStatus")
1791 .and_then(|s| s.as_i64())
1792 .map(|s| s == 1)
1793 .unwrap_or(true);
1794
1795 Ok(AccountInfo {
1796 account_type,
1797 can_trade,
1798 can_withdraw: true,
1799 can_deposit: true,
1800 maker_commission: 0.1, taker_commission: 0.1,
1802 balances,
1803 })
1804 }
1805
1806 async fn get_fees(&self, symbol: Option<&str>) -> ExchangeResult<FeeInfo> {
1807 let mut params = HashMap::new();
1808 params.insert("category".to_string(), "spot".to_string());
1809 if let Some(s) = symbol {
1810 params.insert("symbol".to_string(), s.to_string());
1811 }
1812
1813 let response = self.get(BybitEndpoint::FeeRate, params).await?;
1814
1815 let result = response.get("result")
1816 .ok_or_else(|| ExchangeError::Parse("Missing result".to_string()))?;
1817 let list = result.get("list")
1818 .and_then(|l| l.as_array())
1819 .and_then(|a| a.first())
1820 .ok_or_else(|| ExchangeError::Parse("Empty fee list".to_string()))?;
1821
1822 let maker_rate = list.get("makerFeeRate")
1823 .and_then(|v| v.as_str())
1824 .and_then(|s| s.parse::<f64>().ok())
1825 .unwrap_or(0.001);
1826
1827 let taker_rate = list.get("takerFeeRate")
1828 .and_then(|v| v.as_str())
1829 .and_then(|s| s.parse::<f64>().ok())
1830 .unwrap_or(0.001);
1831
1832 Ok(FeeInfo {
1833 maker_rate,
1834 taker_rate,
1835 symbol: symbol.map(String::from),
1836 tier: None,
1837 })
1838 }
1839
1840 fn account_capabilities(&self, account_type: AccountType) -> AccountCapabilities {
1841 let is_futures = !matches!(account_type, AccountType::Spot | AccountType::Margin);
1842 AccountCapabilities {
1843 has_balances: true,
1844 has_account_info: true,
1845 has_fees: true,
1846 has_transfers: true,
1848 has_sub_accounts: true,
1850 has_deposit_withdraw: true,
1852 has_margin: false,
1854 has_earn_staking: false,
1856 has_funding_history: is_futures,
1859 has_ledger: true,
1861 has_convert: false,
1863 has_positions: is_futures,
1865 }
1866 }
1867}
1868
1869#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
1874#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
1875impl Positions for BybitConnector {
1876 async fn get_positions(&self, query: PositionQuery) -> ExchangeResult<Vec<Position>> {
1877 let symbol = query.symbol.clone();
1878 let account_type = query.account_type;
1879
1880 match account_type {
1881 AccountType::Spot | AccountType::Margin => {
1882 return Err(ExchangeError::UnsupportedOperation(
1883 "Positions not supported for Spot/Margin".to_string()
1884 ));
1885 }
1886 _ => {}
1887 }
1888
1889 let mut params = HashMap::new();
1890 params.insert("category".to_string(), account_type_to_category(account_type).to_string());
1891
1892 if let Some(ref s) = symbol {
1893 params.insert("symbol".to_string(), format_symbol(s, account_type));
1894 }
1895
1896 let response = self.get(BybitEndpoint::Positions, params).await?;
1897
1898 let result = response.get("result")
1900 .ok_or_else(|| ExchangeError::Parse("Missing result".to_string()))?;
1901
1902 let list = result.get("list")
1903 .and_then(|l| l.as_array())
1904 .ok_or_else(|| ExchangeError::Parse("Missing list".to_string()))?;
1905
1906 let mut positions = Vec::new();
1907 for pos_data in list {
1908 let symbol_str = pos_data.get("symbol")
1909 .and_then(|s| s.as_str())
1910 .unwrap_or("");
1911
1912 let quantity = pos_data.get("size")
1913 .and_then(|s| s.as_str())
1914 .and_then(|s| s.parse::<f64>().ok())
1915 .unwrap_or(0.0);
1916
1917 if quantity == 0.0 {
1919 continue;
1920 }
1921
1922 let side = pos_data.get("side")
1923 .and_then(|s| s.as_str())
1924 .map(|s| match s {
1925 "Buy" => crate::core::PositionSide::Long,
1926 "Sell" => crate::core::PositionSide::Short,
1927 _ => crate::core::PositionSide::Long,
1928 })
1929 .unwrap_or(crate::core::PositionSide::Long);
1930
1931 let entry_price = pos_data.get("avgPrice")
1932 .and_then(|p| p.as_str())
1933 .and_then(|s| s.parse::<f64>().ok())
1934 .unwrap_or(0.0);
1935
1936 let unrealized_pnl = pos_data.get("unrealisedPnl")
1937 .and_then(|p| p.as_str())
1938 .and_then(|s| s.parse::<f64>().ok())
1939 .unwrap_or(0.0);
1940
1941 let leverage = pos_data.get("leverage")
1942 .and_then(|l| l.as_str())
1943 .and_then(|s| s.parse::<u32>().ok())
1944 .unwrap_or(1);
1945
1946 let liquidation_price = pos_data.get("liqPrice")
1947 .and_then(|p| p.as_str())
1948 .and_then(|s| s.parse::<f64>().ok());
1949
1950 let mark_price = pos_data.get("markPrice")
1951 .and_then(|p| p.as_str())
1952 .and_then(|s| s.parse::<f64>().ok());
1953
1954 let margin_type = match account_type {
1955 AccountType::FuturesCross => crate::core::MarginType::Cross,
1956 AccountType::FuturesIsolated => crate::core::MarginType::Isolated,
1957 _ => crate::core::MarginType::Cross,
1958 };
1959
1960 positions.push(Position {
1961 symbol: symbol_str.to_string(),
1962 side,
1963 quantity,
1964 entry_price,
1965 mark_price,
1966 unrealized_pnl,
1967 realized_pnl: None,
1968 liquidation_price,
1969 leverage,
1970 margin_type,
1971 margin: None,
1972 take_profit: None,
1973 stop_loss: None,
1974 });
1975 }
1976
1977 Ok(positions)
1978
1979 }
1980
1981 async fn get_funding_rate(
1982 &self,
1983 symbol: &str,
1984 account_type: AccountType,
1985 ) -> ExchangeResult<FundingRate> {
1986 let symbol_str = symbol;
1988 let symbol = {
1989 let parts: Vec<&str> = symbol_str.split('/').collect();
1990 if parts.len() == 2 {
1991 crate::core::Symbol::new(parts[0], parts[1])
1992 } else {
1993 crate::core::Symbol { base: symbol_str.to_string(), quote: String::new(), raw: Some(symbol_str.to_string()) }
1994 }
1995 };
1996
1997 match account_type {
1998 AccountType::Spot | AccountType::Margin => {
1999 return Err(ExchangeError::UnsupportedOperation(
2000 "Funding rate not supported for Spot/Margin".to_string()
2001 ));
2002 }
2003 _ => {}
2004 }
2005
2006 let mut params = HashMap::new();
2007 params.insert("category".to_string(), account_type_to_category(account_type).to_string());
2008 params.insert("symbol".to_string(), format_symbol(&symbol, account_type));
2009
2010 let response = self.get(BybitEndpoint::FundingRate, params).await?;
2011 BybitParser::parse_funding_rate(&response)
2012
2013 }
2014
2015 async fn get_mark_price(
2016 &self,
2017 symbol: &str,
2018 ) -> ExchangeResult<MarkPrice> {
2019 let mut params = HashMap::new();
2022 params.insert("category".to_string(), "linear".to_string());
2023 params.insert("symbol".to_string(), symbol.to_string());
2024
2025 let response = self.get(BybitEndpoint::Ticker, params).await?;
2026
2027 let result = response
2028 .get("result")
2029 .ok_or_else(|| ExchangeError::Parse("Missing result".to_string()))?;
2030 let list = result
2031 .get("list")
2032 .and_then(|l| l.as_array())
2033 .ok_or_else(|| ExchangeError::Parse("Missing result.list".to_string()))?;
2034 let data = list
2035 .first()
2036 .ok_or_else(|| ExchangeError::Parse("Empty result.list".to_string()))?;
2037
2038 let mark_price = data
2039 .get("markPrice")
2040 .and_then(|v| v.as_str())
2041 .and_then(|s| s.parse::<f64>().ok())
2042 .ok_or_else(|| ExchangeError::Parse("Missing markPrice".to_string()))?;
2043
2044 Ok(MarkPrice {
2045 mark_price,
2046 index_price: data
2047 .get("indexPrice")
2048 .and_then(|v| v.as_str())
2049 .and_then(|s| s.parse::<f64>().ok()),
2050 funding_rate: data
2051 .get("fundingRate")
2052 .and_then(|v| v.as_str())
2053 .and_then(|s| s.parse::<f64>().ok()),
2054 timestamp: crate::core::timestamp_millis() as i64,
2055 })
2056 }
2057
2058 async fn get_open_interest(
2059 &self,
2060 symbol: &str,
2061 account_type: AccountType,
2062 ) -> ExchangeResult<OpenInterest> {
2063 let parts: Vec<&str> = symbol.split('/').collect();
2064 let raw_symbol = if parts.len() == 2 {
2065 let sym = crate::core::Symbol::new(parts[0], parts[1]);
2066 format_symbol(&sym, account_type)
2067 } else {
2068 symbol.to_uppercase()
2069 };
2070
2071 let mut params = HashMap::new();
2072 params.insert("category".to_string(), account_type_to_category(account_type).to_string());
2073 params.insert("symbol".to_string(), raw_symbol.clone());
2074
2075 let response = self.get(BybitEndpoint::Ticker, params).await?;
2076
2077 let list = response
2078 .get("result")
2079 .and_then(|r| r.get("list"))
2080 .and_then(|l| l.as_array())
2081 .ok_or_else(|| ExchangeError::Parse("Bybit OI: missing result.list".to_string()))?;
2082
2083 let item = list.first()
2084 .ok_or_else(|| ExchangeError::Parse("Bybit OI: empty list".to_string()))?;
2085
2086 let oi = item.get("openInterest")
2087 .and_then(|v| v.as_str())
2088 .and_then(|s| s.parse::<f64>().ok())
2089 .or_else(|| item.get("openInterest").and_then(|v| v.as_f64()))
2090 .unwrap_or(0.0);
2091
2092 let ts = item.get("time")
2093 .and_then(|v| v.as_i64())
2094 .unwrap_or_else(|| crate::core::timestamp_millis() as i64);
2095
2096 Ok(OpenInterest {
2097 open_interest: oi,
2098 open_interest_value: None,
2099 timestamp: ts,
2100 })
2101 }
2102
2103 async fn modify_position(&self, req: PositionModification) -> ExchangeResult<()> {
2104 match req {
2105 PositionModification::SetLeverage { ref symbol, leverage, account_type } => {
2106 let symbol = symbol.clone();
2107
2108 match account_type {
2109 AccountType::Spot | AccountType::Margin => {
2110 return Err(ExchangeError::UnsupportedOperation(
2111 "Leverage not supported for Spot/Margin".to_string()
2112 ));
2113 }
2114 _ => {}
2115 }
2116
2117 let body = json!({
2118 "category": account_type_to_category(account_type),
2119 "symbol": format_symbol(&symbol, account_type),
2120 "buyLeverage": leverage.to_string(),
2121 "sellLeverage": leverage.to_string(),
2122 });
2123
2124 let response = self.post(BybitEndpoint::SetLeverage, body).await?;
2125 self.check_response(&response)?;
2126 Ok(())
2127 }
2128 PositionModification::SetMarginMode { ref symbol, margin_type, account_type } => {
2129 let symbol = symbol.clone();
2130
2131 match account_type {
2132 AccountType::Spot | AccountType::Margin => {
2133 return Err(ExchangeError::UnsupportedOperation(
2134 "SetMarginMode not supported for Spot/Margin".to_string()
2135 ));
2136 }
2137 _ => {}
2138 }
2139
2140 let trade_mode = match margin_type {
2141 MarginType::Cross => 0i32,
2142 MarginType::Isolated => 1i32,
2143 };
2144
2145 let body = json!({
2146 "category": "linear",
2147 "symbol": format_symbol(&symbol, account_type),
2148 "tradeMode": trade_mode,
2149 "buyLeverage": "1",
2150 "sellLeverage": "1",
2151 });
2152
2153 let response = self.post(BybitEndpoint::SetMarginMode, body).await?;
2154 self.check_response(&response)?;
2155 Ok(())
2156 }
2157 PositionModification::AddMargin { ref symbol, amount, account_type } => {
2158 let symbol = symbol.clone();
2159
2160 match account_type {
2161 AccountType::Spot | AccountType::Margin => {
2162 return Err(ExchangeError::UnsupportedOperation(
2163 "AddMargin not supported for Spot/Margin".to_string()
2164 ));
2165 }
2166 _ => {}
2167 }
2168
2169 let body = json!({
2170 "category": "linear",
2171 "symbol": format_symbol(&symbol, account_type),
2172 "margin": amount.to_string(),
2173 "positionIdx": 0,
2174 });
2175
2176 let response = self.post(BybitEndpoint::AddMargin, body).await?;
2177 self.check_response(&response)?;
2178 Ok(())
2179 }
2180 PositionModification::RemoveMargin { ref symbol, amount, account_type } => {
2181 let symbol = symbol.clone();
2182
2183 match account_type {
2184 AccountType::Spot | AccountType::Margin => {
2185 return Err(ExchangeError::UnsupportedOperation(
2186 "RemoveMargin not supported for Spot/Margin".to_string()
2187 ));
2188 }
2189 _ => {}
2190 }
2191
2192 let body = json!({
2194 "category": "linear",
2195 "symbol": format_symbol(&symbol, account_type),
2196 "margin": format!("-{}", amount),
2197 "positionIdx": 0,
2198 });
2199
2200 let response = self.post(BybitEndpoint::AddMargin, body).await?;
2201 self.check_response(&response)?;
2202 Ok(())
2203 }
2204 PositionModification::ClosePosition { ref symbol, account_type } => {
2205 let symbol = symbol.clone();
2206
2207 match account_type {
2208 AccountType::Spot | AccountType::Margin => {
2209 return Err(ExchangeError::UnsupportedOperation(
2210 "ClosePosition not supported for Spot/Margin".to_string()
2211 ));
2212 }
2213 _ => {}
2214 }
2215
2216 let order_link_id = format!("close_{}", crate::core::timestamp_millis());
2217 let body = json!({
2218 "category": "linear",
2219 "symbol": format_symbol(&symbol, account_type),
2220 "side": "Sell", "orderType": "Market",
2222 "qty": "0",
2223 "reduceOnly": true,
2224 "closeOnTrigger": true,
2225 "orderLinkId": order_link_id,
2226 });
2227
2228 let response = self.post(BybitEndpoint::PlaceOrder, body).await?;
2229 self.check_response(&response)?;
2230 Ok(())
2231 }
2232 PositionModification::SetTpSl { ref symbol, take_profit, stop_loss, account_type } => {
2233 let symbol = symbol.clone();
2234
2235 match account_type {
2236 AccountType::Spot | AccountType::Margin => {
2237 return Err(ExchangeError::UnsupportedOperation(
2238 "SetTpSl not supported for Spot/Margin".to_string()
2239 ));
2240 }
2241 _ => {}
2242 }
2243
2244 let mut body = json!({
2245 "category": "linear",
2246 "symbol": format_symbol(&symbol, account_type),
2247 "positionIdx": 0,
2248 "tpslMode": "Full",
2249 });
2250
2251 if let Some(tp) = take_profit {
2252 body["takeProfit"] = json!(tp.to_string());
2253 }
2254 if let Some(sl) = stop_loss {
2255 body["stopLoss"] = json!(sl.to_string());
2256 }
2257
2258 let response = self.post(BybitEndpoint::TpSlMode, body).await?;
2259 self.check_response(&response)?;
2260 Ok(())
2261 }
2262 _ => Err(ExchangeError::UnsupportedOperation(
2263 "This position modification is not supported by Bybit".to_string()
2264 )),
2265 }
2266 }
2267
2268 async fn get_long_short_ratio(
2269 &self,
2270 symbol: &str,
2271 account_type: AccountType,
2272 ) -> ExchangeResult<crate::core::types::LongShortRatio> {
2273 let category = account_type_to_category(account_type);
2274 let vec = self.get_long_short_ratio(category, symbol, "5min", Some(1)).await?;
2275 vec.into_iter().next().ok_or_else(|| {
2276 crate::core::types::ExchangeError::NotFound(
2277 format!("No long/short ratio data for {symbol}"),
2278 )
2279 })
2280 }
2281}
2282
2283#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
2293#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
2294impl CancelAll for BybitConnector {
2295 async fn cancel_all_orders(
2296 &self,
2297 scope: CancelScope,
2298 account_type: AccountType,
2299 ) -> ExchangeResult<CancelAllResponse> {
2300 let symbol = match &scope {
2301 CancelScope::All { symbol } => symbol.clone(),
2302 CancelScope::BySymbol { symbol } => Some(symbol.clone()),
2303 _ => {
2304 return Err(ExchangeError::InvalidRequest(
2305 "cancel_all_orders only accepts All or BySymbol scope".to_string()
2306 ));
2307 }
2308 };
2309
2310 let mut body = json!({
2311 "category": account_type_to_category(account_type),
2312 });
2313
2314 if let Some(sym) = symbol {
2315 body["symbol"] = json!(format_symbol(&sym, account_type));
2316 }
2317
2318 let response = self.post(BybitEndpoint::CancelAllOrders, body).await?;
2319 BybitParser::parse_cancel_all_response(&response)
2320 }
2321}
2322
2323#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
2332#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
2333impl AmendOrder for BybitConnector {
2334 async fn amend_order(&self, req: AmendRequest) -> ExchangeResult<Order> {
2335 if req.fields.price.is_none() && req.fields.quantity.is_none() && req.fields.trigger_price.is_none() {
2336 return Err(ExchangeError::InvalidRequest(
2337 "At least one of price, quantity, or trigger_price must be provided for amend".to_string()
2338 ));
2339 }
2340
2341 let account_type = req.account_type;
2342 let amend_symbol_str = format_symbol(&req.symbol, account_type);
2343 let mut body = json!({
2344 "category": account_type_to_category(account_type),
2345 "symbol": amend_symbol_str.clone(),
2346 "orderId": req.order_id,
2347 });
2348
2349 if let Some(price) = req.fields.price {
2350 body["price"] = json!(self.precision.price(&amend_symbol_str, price));
2351 }
2352 if let Some(qty) = req.fields.quantity {
2353 body["qty"] = json!(self.precision.qty(&amend_symbol_str, qty));
2354 }
2355 if let Some(trigger_price) = req.fields.trigger_price {
2356 body["triggerPrice"] = json!(self.precision.price(&amend_symbol_str, trigger_price));
2357 }
2358
2359 let response = self.post(BybitEndpoint::AmendOrder, body).await?;
2360 BybitParser::parse_amend_order_response(&response)
2361 }
2362}
2363
2364#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
2373#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
2374impl BatchOrders for BybitConnector {
2375 async fn place_orders_batch(
2376 &self,
2377 orders: Vec<OrderRequest>,
2378 ) -> ExchangeResult<Vec<OrderResult>> {
2379 if orders.is_empty() {
2380 return Ok(vec![]);
2381 }
2382
2383 if orders.len() > self.max_batch_place_size() {
2384 return Err(ExchangeError::InvalidRequest(
2385 format!("Batch size {} exceeds Bybit limit of {}", orders.len(), self.max_batch_place_size())
2386 ));
2387 }
2388
2389 let account_type = orders[0].account_type;
2390 let category = account_type_to_category(account_type);
2391
2392 let order_list: Vec<serde_json::Value> = orders.iter().map(|req| {
2393 let mut obj = serde_json::Map::new();
2394 obj.insert("category".to_string(), json!(category));
2395 obj.insert("symbol".to_string(), json!(format_symbol(&req.symbol, req.account_type)));
2396 obj.insert("side".to_string(), json!(match req.side {
2397 OrderSide::Buy => "Buy",
2398 OrderSide::Sell => "Sell",
2399 }));
2400
2401 let batch_sym_str = format_symbol(&req.symbol, req.account_type);
2402 match &req.order_type {
2403 OrderType::Market => {
2404 obj.insert("orderType".to_string(), json!("Market"));
2405 obj.insert("qty".to_string(), json!(self.precision.qty(&batch_sym_str, req.quantity)));
2406 }
2407 OrderType::Limit { price } => {
2408 obj.insert("orderType".to_string(), json!("Limit"));
2409 obj.insert("qty".to_string(), json!(self.precision.qty(&batch_sym_str, req.quantity)));
2410 obj.insert("price".to_string(), json!(self.precision.price(&batch_sym_str, *price)));
2411 obj.insert("timeInForce".to_string(), json!("GTC"));
2412 }
2413 _ => {
2414 obj.insert("orderType".to_string(), json!("Market"));
2415 obj.insert("qty".to_string(), json!(self.precision.qty(&batch_sym_str, req.quantity)));
2416 }
2417 }
2418
2419 if req.reduce_only {
2420 obj.insert("reduceOnly".to_string(), json!(true));
2421 }
2422 if let Some(ref cid) = req.client_order_id {
2423 obj.insert("orderLinkId".to_string(), json!(cid));
2424 }
2425
2426 serde_json::Value::Object(obj)
2427 }).collect();
2428
2429 let body = json!({
2430 "category": category,
2431 "request": order_list,
2432 });
2433
2434 let response = self.post(BybitEndpoint::BatchPlaceOrders, body).await?;
2435 BybitParser::parse_batch_orders_response(&response)
2436 }
2437
2438 async fn cancel_orders_batch(
2439 &self,
2440 order_ids: Vec<String>,
2441 symbol: Option<&str>,
2442 account_type: AccountType,
2443 ) -> ExchangeResult<Vec<OrderResult>> {
2444 if order_ids.is_empty() {
2445 return Ok(vec![]);
2446 }
2447
2448 if order_ids.len() > self.max_batch_cancel_size() {
2449 return Err(ExchangeError::InvalidRequest(
2450 format!("Batch cancel size {} exceeds Bybit limit of {}", order_ids.len(), self.max_batch_cancel_size())
2451 ));
2452 }
2453
2454 let category = account_type_to_category(account_type);
2455 let sym = symbol.ok_or_else(|| ExchangeError::InvalidRequest(
2456 "Symbol is required for batch cancel on Bybit".to_string()
2457 ))?;
2458
2459 let cancel_list: Vec<serde_json::Value> = order_ids.iter().map(|id| {
2460 json!({
2461 "symbol": sym.replace('/', "").to_uppercase(),
2462 "orderId": id,
2463 })
2464 }).collect();
2465
2466 let body = json!({
2467 "category": category,
2468 "request": cancel_list,
2469 });
2470
2471 let response = self.post(BybitEndpoint::BatchCancelOrders, body).await?;
2472 BybitParser::parse_batch_orders_response(&response)
2473 }
2474
2475 fn max_batch_place_size(&self) -> usize {
2476 10 }
2478
2479 fn max_batch_cancel_size(&self) -> usize {
2480 10 }
2482}
2483
2484impl BybitConnector {
2489 pub async fn batch_amend_orders(
2499 &self,
2500 amends: Vec<serde_json::Value>,
2501 account_type: AccountType,
2502 ) -> ExchangeResult<Value> {
2503 if amends.is_empty() {
2504 return Ok(serde_json::Value::Array(vec![]));
2505 }
2506 if amends.len() > 10 {
2507 return Err(ExchangeError::InvalidRequest(
2508 format!("Batch amend size {} exceeds Bybit limit of 10", amends.len())
2509 ));
2510 }
2511
2512 let category = account_type_to_category(account_type);
2513 let body = json!({
2514 "category": category,
2515 "request": amends,
2516 });
2517
2518 self.post(BybitEndpoint::BatchAmendOrders, body).await
2519 }
2520}
2521
2522#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
2531#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
2532impl AccountTransfers for BybitConnector {
2533 async fn transfer(&self, req: TransferRequest) -> ExchangeResult<TransferResponse> {
2534 let transfer_id = format!("t{}", crate::core::timestamp_millis());
2536
2537 let body = serde_json::json!({
2538 "transferId": transfer_id,
2539 "coin": req.asset,
2540 "amount": req.amount.to_string(),
2541 "fromAccountType": account_type_to_transfer_type(req.from_account),
2542 "toAccountType": account_type_to_transfer_type(req.to_account),
2543 });
2544
2545 let response = self.post(BybitEndpoint::InterTransfer, body).await?;
2546 let mut result = BybitParser::parse_transfer_response(&response)?;
2547
2548 result.asset = req.asset;
2550 result.amount = req.amount;
2551 Ok(result)
2552 }
2553
2554 async fn get_transfer_history(
2555 &self,
2556 filter: TransferHistoryFilter,
2557 ) -> ExchangeResult<Vec<TransferResponse>> {
2558 let mut params = HashMap::new();
2559
2560 if let Some(st) = filter.start_time {
2561 params.insert("startTime".to_string(), st.to_string());
2562 }
2563 if let Some(et) = filter.end_time {
2564 params.insert("endTime".to_string(), et.to_string());
2565 }
2566 if let Some(lim) = filter.limit {
2567 params.insert("limit".to_string(), lim.to_string());
2568 }
2569
2570 let response = self.get(BybitEndpoint::TransferHistory, params).await?;
2571 BybitParser::parse_transfer_history(&response)
2572 }
2573}
2574
2575#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
2586#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
2587impl CustodialFunds for BybitConnector {
2588 async fn get_deposit_address(
2589 &self,
2590 asset: &str,
2591 network: Option<&str>,
2592 ) -> ExchangeResult<DepositAddress> {
2593 let mut params = HashMap::new();
2594 params.insert("coin".to_string(), asset.to_uppercase());
2595
2596 if let Some(net) = network {
2597 params.insert("chainType".to_string(), net.to_string());
2598 }
2599
2600 let response = self.get(BybitEndpoint::DepositAddress, params).await?;
2601 BybitParser::parse_deposit_address(&response, asset, network)
2602 }
2603
2604 async fn withdraw(&self, req: WithdrawRequest) -> ExchangeResult<WithdrawResponse> {
2605 let mut body = serde_json::json!({
2606 "coin": req.asset,
2607 "amount": req.amount.to_string(),
2608 "address": req.address,
2609 "forceChain": 1,
2610 });
2611
2612 if let Some(net) = req.network {
2613 body["chain"] = serde_json::Value::String(net);
2614 }
2615 if let Some(tag) = req.tag {
2616 body["tag"] = serde_json::Value::String(tag);
2617 }
2618
2619 let response = self.post(BybitEndpoint::Withdraw, body).await?;
2620 BybitParser::parse_withdraw_response(&response)
2621 }
2622
2623 async fn get_funds_history(
2624 &self,
2625 filter: FundsHistoryFilter,
2626 ) -> ExchangeResult<Vec<FundsRecord>> {
2627 match filter.record_type {
2628 FundsRecordType::Deposit => {
2629 let mut params = HashMap::new();
2630 if let Some(ref asset) = filter.asset {
2631 params.insert("coin".to_string(), asset.clone());
2632 }
2633 if let Some(st) = filter.start_time {
2634 params.insert("startTime".to_string(), st.to_string());
2635 }
2636 if let Some(et) = filter.end_time {
2637 params.insert("endTime".to_string(), et.to_string());
2638 }
2639 if let Some(lim) = filter.limit {
2640 params.insert("limit".to_string(), lim.to_string());
2641 }
2642
2643 let response = self.get(BybitEndpoint::DepositHistory, params).await?;
2644 BybitParser::parse_deposit_history(&response)
2645 }
2646 FundsRecordType::Withdrawal => {
2647 let mut params = HashMap::new();
2648 if let Some(ref asset) = filter.asset {
2649 params.insert("coin".to_string(), asset.clone());
2650 }
2651 if let Some(st) = filter.start_time {
2652 params.insert("startTime".to_string(), st.to_string());
2653 }
2654 if let Some(et) = filter.end_time {
2655 params.insert("endTime".to_string(), et.to_string());
2656 }
2657 if let Some(lim) = filter.limit {
2658 params.insert("limit".to_string(), lim.to_string());
2659 }
2660
2661 let response = self.get(BybitEndpoint::WithdrawHistory, params).await?;
2662 BybitParser::parse_withdrawal_history(&response)
2663 }
2664 FundsRecordType::Both => {
2665 let deposit_filter = FundsHistoryFilter {
2667 record_type: FundsRecordType::Deposit,
2668 ..filter.clone()
2669 };
2670 let withdrawal_filter = FundsHistoryFilter {
2671 record_type: FundsRecordType::Withdrawal,
2672 ..filter
2673 };
2674
2675 let mut deposits = self.get_funds_history(deposit_filter).await?;
2676 let withdrawals = self.get_funds_history(withdrawal_filter).await?;
2677 deposits.extend(withdrawals);
2678 Ok(deposits)
2679 }
2680 }
2681 }
2682}
2683
2684#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
2695#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
2696impl SubAccounts for BybitConnector {
2697 async fn sub_account_operation(
2698 &self,
2699 op: SubAccountOperation,
2700 ) -> ExchangeResult<SubAccountResult> {
2701 match op {
2702 SubAccountOperation::Create { label } => {
2703 let body = serde_json::json!({
2704 "username": label,
2705 "memberType": 1, });
2707
2708 let response = self.post(BybitEndpoint::CreateSubMember, body).await?;
2709 BybitParser::parse_create_sub_member(&response)
2710 }
2711
2712 SubAccountOperation::List => {
2713 let response = self.get(BybitEndpoint::ListSubMembers, HashMap::new()).await?;
2714 BybitParser::parse_list_sub_members(&response)
2715 }
2716
2717 SubAccountOperation::Transfer { sub_account_id, asset, amount, to_sub } => {
2718 let transfer_id = format!("u{}", crate::core::timestamp_millis());
2725
2726 let (from_member, to_member) = if to_sub {
2727 ("".to_string(), sub_account_id.clone())
2728 } else {
2729 (sub_account_id.clone(), "".to_string())
2730 };
2731
2732 let body = serde_json::json!({
2733 "transferId": transfer_id,
2734 "coin": asset,
2735 "amount": amount.to_string(),
2736 "fromMemberId": from_member,
2737 "toMemberId": to_member,
2738 "fromAccountType": "UNIFIED",
2739 "toAccountType": "UNIFIED",
2740 });
2741
2742 let response = self.post(BybitEndpoint::UniversalTransfer, body).await?;
2743 BybitParser::parse_universal_transfer(&response)
2744 }
2745
2746 SubAccountOperation::GetBalance { sub_account_id } => {
2747 let mut params = HashMap::new();
2748 params.insert("memberId".to_string(), sub_account_id);
2749 params.insert("accountType".to_string(), "UNIFIED".to_string());
2750
2751 let response = self.get(BybitEndpoint::SubAccountBalance, params).await?;
2752 BybitParser::parse_sub_account_balance(&response)
2753 }
2754 }
2755 }
2756}
2757
2758#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
2764#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
2765impl FundingHistory for BybitConnector {
2766 async fn get_funding_payments(
2767 &self,
2768 filter: FundingFilter,
2769 account_type: AccountType,
2770 ) -> ExchangeResult<Vec<FundingPayment>> {
2771 let mut params: HashMap<String, String> = HashMap::new();
2772 params.insert("type".to_string(), "SETTLEMENT".to_string());
2773
2774 let acct_type_str = match account_type {
2775 AccountType::Spot => "SPOT",
2776 _ => "UNIFIED",
2777 };
2778 params.insert("accountType".to_string(), acct_type_str.to_string());
2779
2780 if let Some(symbol) = &filter.symbol {
2781 params.insert("symbol".to_string(), symbol.to_uppercase());
2782 }
2783 if let Some(start) = filter.start_time {
2784 params.insert("startTime".to_string(), start.to_string());
2785 }
2786 if let Some(end) = filter.end_time {
2787 params.insert("endTime".to_string(), end.to_string());
2788 }
2789 if let Some(limit) = filter.limit {
2790 params.insert("limit".to_string(), limit.min(50).to_string());
2791 }
2792
2793 let response = self.get(BybitEndpoint::TransactionLog, params).await?;
2794 BybitParser::parse_funding_payments(&response)
2795 }
2796}
2797
2798#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
2804#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
2805impl AccountLedger for BybitConnector {
2806 async fn get_ledger(
2807 &self,
2808 filter: LedgerFilter,
2809 account_type: AccountType,
2810 ) -> ExchangeResult<Vec<LedgerEntry>> {
2811 let mut params: HashMap<String, String> = HashMap::new();
2812
2813 let acct_type_str = match account_type {
2814 AccountType::Spot => "SPOT",
2815 _ => "UNIFIED",
2816 };
2817 params.insert("accountType".to_string(), acct_type_str.to_string());
2818
2819 if let Some(asset) = &filter.asset {
2820 params.insert("currency".to_string(), asset.to_uppercase());
2821 }
2822 if let Some(start) = filter.start_time {
2823 params.insert("startTime".to_string(), start.to_string());
2824 }
2825 if let Some(end) = filter.end_time {
2826 params.insert("endTime".to_string(), end.to_string());
2827 }
2828 if let Some(limit) = filter.limit {
2829 params.insert("limit".to_string(), limit.min(50).to_string());
2830 }
2831
2832 let response = self.get(BybitEndpoint::TransactionLog, params).await?;
2833 let mut entries = BybitParser::parse_ledger(&response)?;
2834
2835 if let Some(ref type_filter) = filter.entry_type {
2836 entries.retain(|e| &e.entry_type == type_filter);
2837 }
2838
2839 Ok(entries)
2840 }
2841}
2842
2843#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
2848#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
2849impl MarketDataPublic for BybitConnector {
2850 async fn get_open_interest_history(
2851 &self,
2852 symbol: SymbolInput<'_>,
2853 period: &str,
2854 start_time: Option<i64>,
2855 end_time: Option<i64>,
2856 limit: Option<u32>,
2857 account_type: AccountType,
2858 ) -> ExchangeResult<Vec<OpenInterest>> {
2859 let symbol = symbol.resolve(ExchangeId::Bybit, account_type)?;
2860 let category = account_type_to_category(account_type);
2861 self.get_open_interest(category, &symbol, period, limit, start_time, end_time).await
2862 }
2863
2864 async fn get_mark_price_klines(
2865 &self,
2866 symbol: SymbolInput<'_>,
2867 interval: &str,
2868 limit: Option<u32>,
2869 account_type: AccountType,
2870 end_time: Option<i64>,
2871 ) -> ExchangeResult<Vec<Kline>> {
2872 let symbol = symbol.resolve(ExchangeId::Bybit, account_type)?;
2873 let category = account_type_to_category(account_type);
2874 self.get_mark_price_kline(category, &symbol, interval, limit, None, end_time).await
2875 }
2876
2877 async fn get_index_price_klines(
2878 &self,
2879 symbol: SymbolInput<'_>,
2880 interval: &str,
2881 limit: Option<u32>,
2882 account_type: AccountType,
2883 end_time: Option<i64>,
2884 ) -> ExchangeResult<Vec<Kline>> {
2885 let symbol = symbol.resolve(ExchangeId::Bybit, account_type)?;
2886 let category = account_type_to_category(account_type);
2887 self.get_index_price_kline(category, &symbol, interval, limit, None, end_time).await
2888 }
2889
2890 async fn get_premium_index_klines(
2891 &self,
2892 symbol: SymbolInput<'_>,
2893 interval: &str,
2894 limit: Option<u32>,
2895 account_type: AccountType,
2896 end_time: Option<i64>,
2897 ) -> ExchangeResult<Vec<Kline>> {
2898 let symbol = symbol.resolve(ExchangeId::Bybit, account_type)?;
2899 let category = account_type_to_category(account_type);
2900 self.get_premium_index_kline(category, &symbol, interval, limit, None, end_time).await
2901 }
2902
2903 async fn get_long_short_ratio_history(
2904 &self,
2905 symbol: SymbolInput<'_>,
2906 period: &str,
2907 _start_time: Option<i64>,
2908 _end_time: Option<i64>,
2909 limit: Option<u32>,
2910 account_type: AccountType,
2911 ) -> ExchangeResult<Vec<LongShortRatio>> {
2912 let symbol = symbol.resolve(ExchangeId::Bybit, account_type)?;
2913 let category = account_type_to_category(account_type);
2915 self.get_long_short_ratio(category, &symbol, period, limit).await
2916 }
2917
2918 async fn get_funding_rate_history(
2919 &self,
2920 symbol: SymbolInput<'_>,
2921 start_time: Option<i64>,
2922 end_time: Option<i64>,
2923 limit: Option<u32>,
2924 account_type: AccountType,
2925 ) -> ExchangeResult<Vec<FundingRate>> {
2926 let symbol = symbol.resolve(ExchangeId::Bybit, account_type)?;
2927 let category = account_type_to_category(account_type);
2928 self.get_funding_rate_history(category, &symbol, start_time, end_time, limit).await
2929 }
2930}
2931
2932impl crate::core::traits::HasCapabilities for BybitConnector {
2937 fn capabilities(&self) -> crate::core::types::ConnectorCapabilities {
2938 crate::core::types::ConnectorCapabilities {
2939 has_ticker: true,
2941 has_orderbook: true,
2942 has_klines: true,
2943 has_recent_trades: false,
2944 has_exchange_info: true,
2945 has_open_interest_history: true,
2949 has_mark_price_klines: true,
2950 has_index_price_klines: true,
2951 has_premium_index_klines: true,
2952 has_long_short_ratio_history: true,
2953 has_funding_rate_history: true,
2954 has_basis_history: false,
2957 has_taker_volume_history: false,
2958 has_liquidation_history: false,
2959 has_premium_index: false,
2960 has_market_order: true,
2962 has_limit_order: true,
2963 has_open_orders: true,
2964 has_order_history: true,
2965 has_user_trades: true,
2966 has_positions: true,
2968 has_mark_price: true,
2969 has_modify_position: true,
2970 has_closed_pnl: true,
2971 has_long_short_ratio: true,
2972 has_cancel_all: true,
2974 has_amend_order: true,
2975 has_batch_place: true,
2976 has_batch_cancel: true,
2977 max_batch_place_size: 10,
2978 max_batch_cancel_size: 10,
2979 has_balance: true,
2981 has_account_info: true,
2982 has_fees: true,
2983 has_transfers: true,
2984 has_deposit_withdraw: true,
2985 has_sub_accounts: true,
2986 has_funding_payments: true,
2987 has_ledger: true,
2988 has_websocket: true,
2990 has_ws_klines: true,
2991 has_ws_trades: true,
2992 has_ws_orderbook: true,
2993 has_ws_ticker: true,
2994 has_ws_mark_price: true,
2995 has_ws_funding_rate: true,
2996 validation: self.validation_status(),
2997 }
2998 }
2999
3000 fn validation_status(&self) -> Option<&'static crate::core::types::ValidationStamp> {
3001 crate::core::utils::validation_snapshot::validation_for(crate::core::types::ExchangeId::Bybit)
3002 }
3003}