Skip to main content

digdigdig3/l3/open/crypto/cex/bybit/
connector.rs

1//! # Bybit Connector
2//!
3//! Implementation of all core traits for Bybit V5 API.
4//!
5//! ## Core Traits
6//! - `ExchangeIdentity` - exchange identification
7//! - `MarketData` - market data
8//! - `Trading` - trading operations
9//! - `Account` - account information
10//! - `Positions` - futures positions
11//!
12//! ## Extended Methods
13//! Additional Bybit-specific methods as struct methods.
14
15use 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
64// ═══════════════════════════════════════════════════════════════════════════════
65// RATE LIMIT CAPABILITIES (static — embedded in binary, no allocation)
66// ═══════════════════════════════════════════════════════════════════════════════
67
68static 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
91// ═══════════════════════════════════════════════════════════════════════════════
92// CONNECTOR
93// ═══════════════════════════════════════════════════════════════════════════════
94
95/// Bybit connector
96pub struct BybitConnector {
97    /// HTTP client
98    http: HttpClient,
99    /// Authentication (None for public methods)
100    auth: Option<BybitAuth>,
101    /// Testnet mode
102    testnet: bool,
103    /// REST base URL override for proxy / Path-B routing.
104    /// When set, replaces `BybitUrls::base_url(self.testnet)` in every request.
105    rest_override: Option<String>,
106    /// Runtime rate limiter (Weight model: 600 weight per 5 seconds)
107    limiter: Arc<Mutex<RuntimeLimiter>>,
108    /// Pressure monitor — gates non-essential requests at >= 90%
109    monitor: Arc<Mutex<RateLimitMonitor>>,
110    /// Per-symbol precision cache (populated from get_exchange_info)
111    precision: crate::core::utils::precision::PrecisionCache,
112}
113
114impl BybitConnector {
115    /// Create new connector
116    pub async fn new(credentials: Option<Credentials>, testnet: bool) -> ExchangeResult<Self> {
117        Self::new_with_override(credentials, testnet, None).await
118    }
119
120    /// Create new connector with optional REST base URL override.
121    ///
122    /// When `rest_override` is `Some(url)`, all REST requests use that URL as
123    /// the base instead of the exchange's native endpoint.
124    pub async fn new_with_override(credentials: Option<Credentials>, testnet: bool, rest_override: Option<String>) -> ExchangeResult<Self> {
125        let http = HttpClient::new(30_000)?; // 30 sec timeout
126
127        let mut auth = credentials.as_ref().map(BybitAuth::new);
128
129        // Sync time with server if we have auth
130        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); // Convert to milliseconds
141                    }
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    /// Create connector only for public methods
161    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    // ═══════════════════════════════════════════════════════════════════════════
167    // HTTP HELPERS
168    // ═══════════════════════════════════════════════════════════════════════════
169
170    /// Sync limiter from Bybit response headers.
171    ///
172    /// Bybit reports: X-Bapi-Limit-Status = remaining, X-Bapi-Limit = total limit.
173    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    /// Wait for rate limit budget. Non-essential requests are dropped at >= 90% utilization.
193    ///
194    /// Returns `true` if acquired, `false` if dropped due to cutoff pressure.
195    /// Trading endpoints should pass `essential: true` to always wait through.
196    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    /// GET request
221    async fn get(
222        &self,
223        endpoint: BybitEndpoint,
224        params: HashMap<String, String>,
225    ) -> ExchangeResult<Value> {
226        // Market data = non-essential: drop at >= 90% utilization to preserve budget for trading
227        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        // Build query string
238        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        // Add auth headers if needed
251        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    /// POST request
266    async fn post(
267        &self,
268        endpoint: BybitEndpoint,
269        body: Value,
270    ) -> ExchangeResult<Value> {
271        // Order placement = essential: always wait, never drop
272        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        // Auth headers
279        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    /// Check response for errors
291    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    // ═══════════════════════════════════════════════════════════════════════════
310    // EXTENDED METHODS (Bybit-specific)
311    // ═══════════════════════════════════════════════════════════════════════════
312
313    /// Get all tickers
314    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    /// Get symbols
359    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    /// Cancel all orders
367    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        // Parse cancelled order IDs
383        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    // ═══════════════════════════════════════════════════════════════════════════
399    // MARKET DATA EXTENSIONS
400    // ═══════════════════════════════════════════════════════════════════════════
401
402    /// Get open interest history for a symbol.
403    ///
404    /// `category`: `"linear"` | `"inverse"`.
405    /// `interval_time`: `"5min"` | `"15min"` | `"30min"` | `"1h"` | `"4h"` | `"1d"`.
406    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    /// Get historical funding rates.
433    ///
434    /// Endpoint: `GET /v5/market/funding/history` — no auth.
435    /// `category`: `"linear"` | `"inverse"`. `symbol`: e.g. `"BTCUSDT"`.
436    /// `limit`: max 200 (default 200). `start_time` / `end_time`: Unix ms.
437    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    /// Get long/short ratio for a symbol.
462    ///
463    /// `category`: `"linear"` | `"inverse"`. Bybit `ratio_type` is always `"account"`.
464    /// `period`: `"5min"` | `"15min"` | `"30min"` | `"1h"` | `"4h"` | `"1d"`.
465    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    /// Get mark price kline data.
484    ///
485    /// `category`: `"linear"` | `"inverse"`.
486    /// `interval`: standard interval string (e.g. `"1m"`, `"1h"`, `"1d"`).
487    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    /// Get index price kline data.
514    ///
515    /// `category`: `"linear"` | `"inverse"`.
516    /// `interval`: standard interval string (e.g. `"1m"`, `"1h"`, `"1d"`).
517    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    /// Get premium index price kline data.
544    ///
545    /// `category`: `"linear"` | `"inverse"`.
546    /// `interval`: standard interval string (e.g. `"1m"`, `"1h"`, `"1d"`).
547    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    // ═══════════════════════════════════════════════════════════════════════════
574    // FILL / TRADE HISTORY
575    // ═══════════════════════════════════════════════════════════════════════════
576
577    /// Get personal trade fills (executions).
578    ///
579    /// `start_time` and `end_time` are Unix milliseconds.
580    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    /// Get closed PnL history for futures positions.
606    ///
607    /// `start_time` and `end_time` are Unix milliseconds.
608    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    /// Get institutional loan product information.
634    ///
635    /// Endpoint: `GET /v5/ins-loan/product-infos` — public, no auth required.
636    /// Returns the list of loan products with leverage and symbol info.
637    pub async fn get_institutional_loan_products(&self) -> ExchangeResult<Value> {
638        self.get(BybitEndpoint::InsLoanProducts, HashMap::new()).await
639    }
640
641    /// Get risk limit tiers for a symbol.
642    ///
643    /// Endpoint: `GET /v5/market/risk-limit` — public, no auth required.
644    /// `category`: `"linear"` | `"inverse"`.
645    /// Returns tier list with `riskLimitValue`, `maintenanceMargin`, `initialMargin`, `maxLeverage`.
646    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    /// Get futures delivery reference price.
658    ///
659    /// Endpoint: `GET /v5/market/delivery-price` — public, no auth required.
660    /// `category`: `"inverse"` for coin-margined futures.
661    /// `symbol`: e.g. `"BTCUSD"` for coin-margined.
662    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
678// ═══════════════════════════════════════════════════════════════════════════════
679// EXCHANGE IDENTITY
680// ═══════════════════════════════════════════════════════════════════════════════
681
682impl 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// ═══════════════════════════════════════════════════════════════════════════════
797// MARKET DATA
798// ═══════════════════════════════════════════════════════════════════════════════
799
800#[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        // Bybit uses the same /v5/market/* endpoints for both Spot and Futures —
891        // the `category` parameter (spot/linear) is passed per-request. Intervals
892        // and limits are identical across categories, so no branching needed here.
893        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            // get_recent_trades is a struct method (not part of MarketData trait impl)
901            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            // map_kline_interval covers: 1m 3m 5m 15m 30m 1h 2h 4h 6h 12h 1d 1w 1M (no 8h/3d)
907            supported_intervals: &[
908                "1m", "3m", "5m", "15m", "30m",
909                "1h", "2h", "4h", "6h", "12h",
910                "1d", "1w", "1M",
911            ],
912            // get_klines caps limit at .min(1000)
913            max_kline_limit: Some(1000),
914        }
915    }
916}
917
918// ═══════════════════════════════════════════════════════════════════════════════
919// TRADING
920// ═══════════════════════════════════════════════════════════════════════════════
921
922#[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                        // Extract order ID from response
951                        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                        // Return minimal order info (can fetch full info with get_order)
960                        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                // Bybit Futures: trailingStop order via conditional order
1135                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                // GTD (Good-Till-Date) is NOT supported by Bybit v5.
1330                // Bybit TimeInForce enum: GTC, IOC, FOK, PostOnly, RPI — no GTD.
1331                // Research confirmed: RESEARCH_WAVE2_BATCH1.md §2.1 "GTD on Bybit: NOT SUPPORTED"
1332                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                // Bybit v5 supports iceberg orders via /v5/order/create.
1392                // Parameters: orderType=Limit, timeInForce=GTC, qty=total, peakOrderQty=visible slice.
1393                // Research confirmed: RESEARCH_WAVE2_BATCH1.md §2.1 "Iceberg on Bybit: supported"
1394                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            // OCO: UI only on Bybit — "API users won't have access to OCO orders"
1440            // Bracket: no native bracket order type on Bybit API
1441            // TWAP: UI-only strategy feature, no public API endpoint
1442            // Research confirmed: RESEARCH_WAVE2_BATCH1.md §2.1
1443            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                // Return a sentinel cancelled order
1552                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                // Bybit V5 does not have a native batch cancel — cancel one by one
1603                // Per rules: must NOT loop cancel. Return UnsupportedOperation.
1604                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        // Parse symbol string into Symbol struct
1622        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        // Convert Option<&str> to Option<Symbol>
1645        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        // Parse all orders from result.list
1665        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            // Create a wrapper to match parser expectations
1675            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            // TrailingStop: Futures only — Spot/Margin returns UnsupportedOperation (line 874)
1738            has_trailing_stop: is_futures,
1739            // Bracket: no native bracket order type on Bybit API (UnsupportedOperation for all)
1740            has_bracket: false,
1741            // OCO: UI only on Bybit — not available via API (UnsupportedOperation for all)
1742            has_oco: false,
1743            // AmendOrder: POST /v5/order/amend supports both spot and linear
1744            has_amend: true,
1745            // BatchOrders: POST /v5/order/create-batch and cancel-batch support both categories
1746            has_batch: true,
1747            // Bybit batch limit is 10 per request (same for spot and futures)
1748            max_batch_size: Some(10),
1749            // CancelAll: POST /v5/order/cancel-all supports both spot and linear
1750            has_cancel_all: true,
1751            has_user_trades: true,
1752            has_order_history: true,
1753        }
1754    }
1755}
1756
1757// ═══════════════════════════════════════════════════════════════════════════════
1758// ACCOUNT
1759// ═══════════════════════════════════════════════════════════════════════════════
1760
1761#[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        // Get balances
1784        let balances = self.get_balance(BalanceQuery { asset: None, account_type }).await?;
1785
1786        // Parse account info from response
1787        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, // Default, should be fetched from API
1801            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            // AccountTransfers: no account_type branching in impl — works for all
1847            has_transfers: true,
1848            // SubAccounts: no account_type branching in impl — works for all
1849            has_sub_accounts: true,
1850            // CustodialFunds: deposit address, withdraw, deposit/withdrawal history — works for all
1851            has_deposit_withdraw: true,
1852            // No MarginTrading trait — no borrow/repay endpoints
1853            has_margin: false,
1854            // No earn/staking endpoints implemented
1855            has_earn_staking: false,
1856            // FundingHistory: funding payments (SETTLEMENT) only exist for Futures positions
1857            // Spot/Margin returns UnsupportedOperation from get_funding_rate (line 1729)
1858            has_funding_history: is_futures,
1859            // AccountLedger: GET /v5/account/transaction-log — available for all account types
1860            has_ledger: true,
1861            // No ConvertSwap trait — no coin conversion endpoints
1862            has_convert: false,
1863            // Positions (GET /v5/position/list) are Futures/Perp only — Spot has no positions.
1864            has_positions: is_futures,
1865        }
1866    }
1867}
1868
1869// ═══════════════════════════════════════════════════════════════════════════════
1870// POSITIONS
1871// ═══════════════════════════════════════════════════════════════════════════════
1872
1873#[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        // Parse positions from result.list
1899        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            // Skip zero positions
1918            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        // Parse symbol string into Symbol struct
1987        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        // GET /v5/market/tickers?category=linear&symbol=BTCUSDT
2020        // Response: {result: {list: [{symbol, markPrice, indexPrice, fundingRate, ...}]}}
2021        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                // Bybit: negative margin amount means remove
2193                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", // Will be auto-corrected by reduceOnly logic
2221                    "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// ═══════════════════════════════════════════════════════════════════════════════
2284// CANCEL ALL
2285// ═══════════════════════════════════════════════════════════════════════════════
2286
2287/// Cancel all open orders via Bybit native endpoint.
2288///
2289/// Bybit: `POST /v5/order/cancel-all`
2290/// Supports both spot and linear (futures).
2291/// `CancelScope::All { symbol: None }` cancels across the entire category.
2292#[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// ═══════════════════════════════════════════════════════════════════════════════
2324// AMEND ORDER
2325// ═══════════════════════════════════════════════════════════════════════════════
2326
2327/// Modify a live order in-place via Bybit native amend endpoint.
2328///
2329/// Bybit: `POST /v5/order/amend`
2330/// Supports spot and linear. At least one of price/quantity must be provided.
2331#[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// ═══════════════════════════════════════════════════════════════════════════════
2365// BATCH ORDERS
2366// ═══════════════════════════════════════════════════════════════════════════════
2367
2368/// Native batch order placement and cancellation via Bybit batch endpoints.
2369///
2370/// Bybit: `POST /v5/order/create-batch` (max 10), `POST /v5/order/cancel-batch` (max 10)
2371/// Both spot and linear categories are supported.
2372#[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 // Bybit limit
2477    }
2478
2479    fn max_batch_cancel_size(&self) -> usize {
2480        10 // Bybit limit
2481    }
2482}
2483
2484// ═══════════════════════════════════════════════════════════════════════════════
2485// BATCH AMEND
2486// ═══════════════════════════════════════════════════════════════════════════════
2487
2488impl BybitConnector {
2489    /// Batch amend multiple orders via `POST /v5/order/amend-batch`.
2490    ///
2491    /// Each entry in `amends` should be a JSON object containing:
2492    /// `category`, `symbol`, `orderId` (or `orderLinkId`), plus at least one of
2493    /// `price` or `qty`.
2494    ///
2495    /// Max 10 orders per batch (Bybit limit).
2496    ///
2497    /// Returns the raw JSON response from Bybit.
2498    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// ═══════════════════════════════════════════════════════════════════════════════
2523// ACCOUNT TRANSFERS
2524// ═══════════════════════════════════════════════════════════════════════════════
2525
2526/// Internal transfers between Bybit account types.
2527///
2528/// Bybit: `POST /v5/asset/transfer/inter-transfer`
2529/// Supports UNIFIED, SPOT, CONTRACT, and FUND account types.
2530#[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        // Generate a unique transfer ID (UUID-like using timestamp)
2535        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        // Fill in the fields that Bybit doesn't echo back in the response
2549        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// ═══════════════════════════════════════════════════════════════════════════════
2576// CUSTODIAL FUNDS
2577// ═══════════════════════════════════════════════════════════════════════════════
2578
2579/// Deposit and withdrawal management for Bybit.
2580///
2581/// - Deposit address: `GET /v5/asset/deposit/query-address`
2582/// - Withdraw: `POST /v5/asset/withdraw/create`
2583/// - Deposit history: `GET /v5/asset/deposit/query-record`
2584/// - Withdrawal history: `GET /v5/asset/withdraw/query-record`
2585#[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                // Bybit has separate endpoints — fetch both and merge
2666                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// ═══════════════════════════════════════════════════════════════════════════════
2685// SUB-ACCOUNTS
2686// ═══════════════════════════════════════════════════════════════════════════════
2687
2688/// Sub-account management for Bybit.
2689///
2690/// - Create: `POST /v5/user/create-sub-member`
2691/// - List: `GET /v5/user/query-sub-members`
2692/// - Transfer: `POST /v5/asset/transfer/universal-transfer`
2693/// - Get balance: `GET /v5/asset/transfer/query-account-coins-balance`
2694#[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,  // 1 = normal sub-account
2706                });
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                // Universal transfer: master ↔ sub-account
2719                // For master → sub: fromMemberId = master UID (empty = self), toMemberId = sub_account_id
2720                // For sub → master: fromMemberId = sub_account_id, toMemberId = master UID (empty = self)
2721                // Bybit universal transfer requires explicit member IDs.
2722                // We use the fromAccountType/toAccountType as UNIFIED for both sides since
2723                // we don't have the user's account type preference here.
2724                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// ═══════════════════════════════════════════════════════════════════════════════
2759// FUNDING HISTORY
2760// ═══════════════════════════════════════════════════════════════════════════════
2761
2762/// Funding payment history via `GET /v5/account/transaction-log?type=SETTLEMENT`
2763#[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// ═══════════════════════════════════════════════════════════════════════════════
2799// ACCOUNT LEDGER
2800// ═══════════════════════════════════════════════════════════════════════════════
2801
2802/// Full account ledger via `GET /v5/account/transaction-log` (all types).
2803#[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// ═══════════════════════════════════════════════════════════════════════════════
2844// MarketDataPublic trait impl
2845// ═══════════════════════════════════════════════════════════════════════════════
2846
2847#[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        // Bybit's get_long_short_ratio does not support start/end time filtering.
2914        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
2932// ═══════════════════════════════════════════════════════════════════════════════
2933// HAS CAPABILITIES
2934// ═══════════════════════════════════════════════════════════════════════════════
2935
2936impl crate::core::traits::HasCapabilities for BybitConnector {
2937    fn capabilities(&self) -> crate::core::types::ConnectorCapabilities {
2938        crate::core::types::ConnectorCapabilities {
2939            // MarketData
2940            has_ticker: true,
2941            has_orderbook: true,
2942            has_klines: true,
2943            has_recent_trades: false,
2944            has_exchange_info: true,
2945            // MarketDataPublic (verified overrides: get_open_interest_history,
2946            //   get_mark_price_klines, get_index_price_klines,
2947            //   get_long_short_ratio_history, get_funding_rate_history)
2948            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: /v5/market/basis returns HTTP 404 on live wire
2955            // (confirmed 2026-06-04); docs page also 404s. Not implemented.
2956            has_basis_history: false,
2957            has_taker_volume_history: false,
2958            has_liquidation_history: false,
2959            has_premium_index: false,
2960            // Trading
2961            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            // Positions
2967            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            // Operations
2973            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            // Account
2980            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            // WebSocket
2989            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}