rusty_bybit/
types.rs

1//! Common data structures for Bybit v5 API
2//!
3//! Defines all request/response types used throughout the SDK.
4//!
5//! # Response Wrappers
6//!
7//! Most API responses use wrapper objects containing a `list` field:
8//! - `TickerList` - wraps `Vec<Ticker>`
9//! - `InstrumentList` - wraps `Vec<InstrumentInfo>`
10//! - `PositionList` - wraps `Vec<Position>`
11//! - `OrderList` - wraps `Vec<Order>`
12//! - `WalletBalance` - wraps `Vec<AccountBalance>`
13
14use serde::{Deserialize, Serialize};
15
16/// Bybit server time response
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct ServerTime {
19    #[serde(rename = "timeSecond")]
20    pub time_second: String,
21    #[serde(rename = "timeNano")]
22    pub time_nano: String,
23}
24
25/// Empty result for API calls that don't return data
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct EmptyResult;
28
29/// Product category for Bybit API endpoints
30#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
31pub enum Category {
32    #[serde(rename = "linear")]
33    Linear,
34    #[serde(rename = "inverse")]
35    Inverse,
36    #[serde(rename = "spot")]
37    Spot,
38    #[serde(rename = "option")]
39    Option,
40}
41
42/// Bybit API response wrapper
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct ApiResponse<T> {
45    #[serde(rename = "retCode")]
46    pub ret_code: i32,
47    #[serde(rename = "retMsg")]
48    pub ret_msg: String,
49    pub result: T,
50    #[serde(rename = "retExtInfo", default)]
51    pub ret_ext_info: serde_json::Value,
52    pub time: i64,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct OrderBook {
57    pub b: Vec<(String, String)>,
58    pub a: Vec<(String, String)>,
59    pub ts: i64,
60    pub u: i64,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct InstrumentInfo {
65    pub symbol: String,
66    #[serde(rename = "contractType")]
67    pub contract_type: String,
68    pub status: String,
69    #[serde(rename = "baseCoin")]
70    pub base_coin: String,
71    #[serde(rename = "quoteCoin")]
72    pub quote_coin: String,
73    #[serde(rename = "settleCoin")]
74    pub settle_coin: String,
75    #[serde(rename = "priceScale")]
76    pub price_scale: String,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct Ticker {
81    pub symbol: String,
82    #[serde(rename = "lastPrice")]
83    pub last_price: String,
84    #[serde(rename = "indexPrice")]
85    pub index_price: String,
86    #[serde(rename = "markPrice")]
87    pub mark_price: String,
88    #[serde(rename = "bid1Price")]
89    pub bid1_price: String,
90    #[serde(rename = "bid1Size")]
91    pub bid1_size: String,
92    #[serde(rename = "ask1Price")]
93    pub ask1_price: String,
94    #[serde(rename = "ask1Size")]
95    pub ask1_size: String,
96}
97
98/// Wrapper for ticker list response
99#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct TickerList {
101    pub list: Vec<Ticker>,
102    pub next_page_cursor: Option<String>,
103}
104
105/// Wrapper for instrument list response
106#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct InstrumentList {
108    pub list: Vec<InstrumentInfo>,
109    pub next_page_cursor: Option<String>,
110}
111
112/// Wrapper for wallet balance response
113#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct WalletBalance {
115    pub list: Vec<AccountBalance>,
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct AccountBalance {
120    #[serde(rename = "accountType")]
121    pub account_type: String,
122    #[serde(rename = "accountIMRate")]
123    pub account_im_rate: String,
124    #[serde(rename = "accountMMRate")]
125    pub account_mm_rate: String,
126    #[serde(rename = "totalEquity")]
127    pub total_equity: String,
128    #[serde(rename = "totalWalletBalance")]
129    pub total_wallet_balance: String,
130    #[serde(rename = "totalMarginBalance")]
131    pub total_margin_balance: String,
132    #[serde(rename = "totalAvailableBalance")]
133    pub total_available_balance: String,
134    #[serde(rename = "totalPerpUPL")]
135    pub total_perp_upl: String,
136    #[serde(rename = "totalInitialMargin")]
137    pub total_initial_margin: String,
138    #[serde(rename = "totalMaintenanceMargin")]
139    pub total_maintenance_margin: String,
140    pub coin: Vec<CoinBalance>,
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize)]
144pub struct CoinBalance {
145    pub coin: String,
146    pub wallet_balance: String,
147    #[serde(rename = "transferBalance")]
148    pub transfer_balance: String,
149}
150
151/// Wrapper for position list response
152#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct PositionList {
154    pub list: Vec<Position>,
155    pub category: String,
156    pub next_page_cursor: Option<String>,
157}
158
159#[derive(Debug, Clone, Serialize, Deserialize)]
160pub struct Position {
161    pub symbol: String,
162    #[serde(rename = "positionIdx")]
163    pub position_idx: u64,
164    #[serde(rename = "positionStatus")]
165    pub position_status: String,
166    pub side: String,
167    pub size: String,
168    #[serde(rename = "positionValue")]
169    pub position_value: String,
170    #[serde(rename = "unrealisedPnl")]
171    pub unrealised_pnl: String,
172}
173
174/// Order side: Buy or Sell
175#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
176pub enum Side {
177    #[serde(rename = "Buy")]
178    Buy,
179    #[serde(rename = "Sell")]
180    Sell,
181}
182
183/// Order type: Market or Limit
184#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
185pub enum OrderType {
186    #[serde(rename = "Market")]
187    Market,
188    #[serde(rename = "Limit")]
189    Limit,
190}
191
192/// Time in force strategy for orders
193#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
194pub enum TimeInForce {
195    #[serde(rename = "GTC")]
196    GTC,
197    #[serde(rename = "IOC")]
198    IOC,
199    #[serde(rename = "FOK")]
200    FOK,
201    #[serde(rename = "PostOnly")]
202    PostOnly,
203    #[serde(rename = "RPI")]
204    RPI,
205}
206
207/// Order status
208#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
209pub enum OrderStatus {
210    #[serde(rename = "New")]
211    New,
212    #[serde(rename = "PartiallyFilled")]
213    PartiallyFilled,
214    #[serde(rename = "Filled")]
215    Filled,
216    #[serde(rename = "Cancelled")]
217    Cancelled,
218    #[serde(rename = "Rejected")]
219    Rejected,
220}
221
222/// Wrapper for order list response
223#[derive(Debug, Clone, Serialize, Deserialize)]
224pub struct OrderList {
225    pub list: Vec<Order>,
226    pub next_page_cursor: Option<String>,
227    pub category: String,
228}
229
230#[derive(Debug, Clone, Serialize, Deserialize)]
231pub struct Order {
232    pub order_id: String,
233    pub order_link_id: String,
234    pub symbol: String,
235    pub side: String,
236    pub order_type: String,
237    pub price: String,
238    pub qty: String,
239    pub time_in_force: String,
240    pub create_type: String,
241    pub cancel_type: String,
242    pub status: String,
243    pub leaves_qty: String,
244    pub cum_exec_qty: String,
245    pub avg_price: String,
246    pub created_time: String,
247    pub updated_time: String,
248    #[serde(rename = "positionIdx")]
249    pub position_idx: u64,
250    #[serde(rename = "triggerPrice")]
251    pub trigger_price: Option<String>,
252    #[serde(rename = "takeProfit")]
253    pub take_profit: Option<String>,
254    #[serde(rename = "stopLoss")]
255    pub stop_loss: Option<String>,
256    #[serde(rename = "reduceOnly")]
257    pub reduce_only: Option<bool>,
258    #[serde(rename = "closeOnTrigger")]
259    pub close_on_trigger: Option<bool>,
260}
261
262#[derive(Debug, Clone, Default, Serialize, Deserialize)]
263pub struct CreateOrderRequest {
264    pub category: String,
265    pub symbol: String,
266    pub side: String,
267    #[serde(rename = "orderType")]
268    pub order_type: String,
269    #[serde(skip_serializing_if = "Option::is_none")]
270    pub qty: Option<String>,
271    #[serde(skip_serializing_if = "Option::is_none")]
272    pub price: Option<String>,
273    #[serde(rename = "timeInForce", skip_serializing_if = "Option::is_none")]
274    pub time_in_force: Option<String>,
275    #[serde(rename = "positionIdx", skip_serializing_if = "Option::is_none")]
276    pub position_idx: Option<u64>,
277    #[serde(rename = "orderLinkId", skip_serializing_if = "Option::is_none")]
278    pub order_link_id: Option<String>,
279    #[serde(rename = "triggerPrice", skip_serializing_if = "Option::is_none")]
280    pub trigger_price: Option<String>,
281    #[serde(rename = "takeProfit", skip_serializing_if = "Option::is_none")]
282    pub take_profit: Option<String>,
283    #[serde(rename = "stopLoss", skip_serializing_if = "Option::is_none")]
284    pub stop_loss: Option<String>,
285    #[serde(rename = "reduceOnly", skip_serializing_if = "Option::is_none")]
286    pub reduce_only: Option<bool>,
287    #[serde(rename = "closeOnTrigger", skip_serializing_if = "Option::is_none")]
288    pub close_on_trigger: Option<bool>,
289    #[serde(skip_serializing_if = "Option::is_none")]
290    pub trigger_by: Option<String>,
291    #[serde(skip_serializing_if = "Option::is_none")]
292    pub tp_trigger_by: Option<String>,
293    #[serde(skip_serializing_if = "Option::is_none")]
294    pub sl_trigger_by: Option<String>,
295    #[serde(skip_serializing_if = "Option::is_none")]
296    pub market_unit: Option<String>,
297    #[serde(skip_serializing_if = "Option::is_none")]
298    pub slippage_tolerance_type: Option<String>,
299    #[serde(skip_serializing_if = "Option::is_none")]
300    pub slippage_tolerance: Option<String>,
301    #[serde(skip_serializing_if = "Option::is_none")]
302    pub trigger_direction: Option<i32>,
303    #[serde(skip_serializing_if = "Option::is_none")]
304    pub order_filter: Option<String>,
305}
306
307impl CreateOrderRequest {
308    pub fn builder() -> CreateOrderRequestBuilder {
309        CreateOrderRequestBuilder::default()
310    }
311}
312
313/// Builder for CreateOrderRequest with fluent API
314#[derive(Debug, Default)]
315pub struct CreateOrderRequestBuilder {
316    category: Option<String>,
317    symbol: Option<String>,
318    side: Option<String>,
319    order_type: Option<String>,
320    qty: Option<String>,
321    price: Option<String>,
322    time_in_force: Option<String>,
323    position_idx: Option<u64>,
324    order_link_id: Option<String>,
325    trigger_price: Option<String>,
326    take_profit: Option<String>,
327    stop_loss: Option<String>,
328    reduce_only: Option<bool>,
329    close_on_trigger: Option<bool>,
330    trigger_by: Option<String>,
331    tp_trigger_by: Option<String>,
332    sl_trigger_by: Option<String>,
333    market_unit: Option<String>,
334    slippage_tolerance_type: Option<String>,
335    slippage_tolerance: Option<String>,
336    trigger_direction: Option<i32>,
337    order_filter: Option<String>,
338}
339
340impl CreateOrderRequestBuilder {
341    pub fn category(mut self, category: impl Into<String>) -> Self {
342        self.category = Some(category.into());
343        self
344    }
345
346    pub fn symbol(mut self, symbol: impl Into<String>) -> Self {
347        self.symbol = Some(symbol.into());
348        self
349    }
350
351    pub fn side(mut self, side: impl Into<String>) -> Self {
352        self.side = Some(side.into());
353        self
354    }
355
356    pub fn order_type(mut self, order_type: impl Into<String>) -> Self {
357        self.order_type = Some(order_type.into());
358        self
359    }
360
361    pub fn qty(mut self, qty: impl Into<String>) -> Self {
362        self.qty = Some(qty.into());
363        self
364    }
365
366    pub fn price(mut self, price: impl Into<String>) -> Self {
367        self.price = Some(price.into());
368        self
369    }
370
371    pub fn time_in_force(mut self, time_in_force: impl Into<String>) -> Self {
372        self.time_in_force = Some(time_in_force.into());
373        self
374    }
375
376    pub fn position_idx(mut self, position_idx: u64) -> Self {
377        self.position_idx = Some(position_idx);
378        self
379    }
380
381    pub fn order_link_id(mut self, order_link_id: impl Into<String>) -> Self {
382        self.order_link_id = Some(order_link_id.into());
383        self
384    }
385
386    pub fn trigger_price(mut self, trigger_price: impl Into<String>) -> Self {
387        self.trigger_price = Some(trigger_price.into());
388        self
389    }
390
391    pub fn take_profit(mut self, take_profit: impl Into<String>) -> Self {
392        self.take_profit = Some(take_profit.into());
393        self
394    }
395
396    pub fn stop_loss(mut self, stop_loss: impl Into<String>) -> Self {
397        self.stop_loss = Some(stop_loss.into());
398        self
399    }
400
401    pub fn reduce_only(mut self, reduce_only: bool) -> Self {
402        self.reduce_only = Some(reduce_only);
403        self
404    }
405
406    pub fn close_on_trigger(mut self, close_on_trigger: bool) -> Self {
407        self.close_on_trigger = Some(close_on_trigger);
408        self
409    }
410
411    pub fn trigger_by(mut self, trigger_by: impl Into<String>) -> Self {
412        self.trigger_by = Some(trigger_by.into());
413        self
414    }
415
416    pub fn tp_trigger_by(mut self, tp_trigger_by: impl Into<String>) -> Self {
417        self.tp_trigger_by = Some(tp_trigger_by.into());
418        self
419    }
420
421    pub fn sl_trigger_by(mut self, sl_trigger_by: impl Into<String>) -> Self {
422        self.sl_trigger_by = Some(sl_trigger_by.into());
423        self
424    }
425
426    pub fn market_unit(mut self, market_unit: impl Into<String>) -> Self {
427        self.market_unit = Some(market_unit.into());
428        self
429    }
430
431    pub fn slippage_tolerance_type(mut self, slippage_tolerance_type: impl Into<String>) -> Self {
432        self.slippage_tolerance_type = Some(slippage_tolerance_type.into());
433        self
434    }
435
436    pub fn slippage_tolerance(mut self, slippage_tolerance: impl Into<String>) -> Self {
437        self.slippage_tolerance = Some(slippage_tolerance.into());
438        self
439    }
440
441    pub fn trigger_direction(mut self, trigger_direction: i32) -> Self {
442        self.trigger_direction = Some(trigger_direction);
443        self
444    }
445
446    pub fn order_filter(mut self, order_filter: impl Into<String>) -> Self {
447        self.order_filter = Some(order_filter.into());
448        self
449    }
450
451    pub fn build(self) -> CreateOrderRequest {
452        CreateOrderRequest {
453            category: self.category.unwrap_or_else(|| "linear".to_string()),
454            symbol: self.symbol.expect("symbol is required"),
455            side: self.side.expect("side is required"),
456            order_type: self.order_type.expect("order_type is required"),
457            qty: self.qty,
458            price: self.price,
459            time_in_force: self.time_in_force,
460            position_idx: self.position_idx,
461            order_link_id: self.order_link_id,
462            trigger_price: self.trigger_price,
463            take_profit: self.take_profit,
464            stop_loss: self.stop_loss,
465            reduce_only: self.reduce_only,
466            close_on_trigger: self.close_on_trigger,
467            trigger_by: self.trigger_by,
468            tp_trigger_by: self.tp_trigger_by,
469            sl_trigger_by: self.sl_trigger_by,
470            market_unit: self.market_unit,
471            slippage_tolerance_type: self.slippage_tolerance_type,
472            slippage_tolerance: self.slippage_tolerance,
473            trigger_direction: self.trigger_direction,
474            order_filter: self.order_filter,
475        }
476    }
477}
478
479#[derive(Debug, Clone, Serialize, Deserialize)]
480pub struct CreateOrderResponse {
481    pub order_id: String,
482    pub order_link_id: String,
483}
484
485#[cfg(test)]
486mod tests {
487    use super::*;
488
489    #[test]
490    fn test_category_serialization() {
491        let linear_json = serde_json::to_string(&Category::Linear).unwrap();
492        assert_eq!(linear_json, r#""linear""#);
493
494        let inverse_json = serde_json::to_string(&Category::Inverse).unwrap();
495        assert_eq!(inverse_json, r#""inverse""#);
496
497        let spot_json = serde_json::to_string(&Category::Spot).unwrap();
498        assert_eq!(spot_json, r#""spot""#);
499    }
500
501    #[test]
502    fn test_category_deserialization() {
503        let linear: Category = serde_json::from_str(r#""linear""#).unwrap();
504        assert_eq!(linear, Category::Linear);
505
506        let inverse: Category = serde_json::from_str(r#""inverse""#).unwrap();
507        assert_eq!(inverse, Category::Inverse);
508
509        let spot: Category = serde_json::from_str(r#""spot""#).unwrap();
510        assert_eq!(spot, Category::Spot);
511    }
512
513    #[test]
514    fn test_side_serialization() {
515        let buy_json = serde_json::to_string(&Side::Buy).unwrap();
516        assert_eq!(buy_json, r#""Buy""#);
517
518        let sell_json = serde_json::to_string(&Side::Sell).unwrap();
519        assert_eq!(sell_json, r#""Sell""#);
520    }
521
522    #[test]
523    fn test_side_deserialization() {
524        let buy: Side = serde_json::from_str(r#""Buy""#).unwrap();
525        assert_eq!(buy, Side::Buy);
526
527        let sell: Side = serde_json::from_str(r#""Sell""#).unwrap();
528        assert_eq!(sell, Side::Sell);
529    }
530
531    #[test]
532    fn test_order_type_serialization() {
533        let market_json = serde_json::to_string(&OrderType::Market).unwrap();
534        assert_eq!(market_json, r#""Market""#);
535
536        let limit_json = serde_json::to_string(&OrderType::Limit).unwrap();
537        assert_eq!(limit_json, r#""Limit""#);
538    }
539
540    #[test]
541    fn test_order_type_deserialization() {
542        let market: OrderType = serde_json::from_str(r#""Market""#).unwrap();
543        assert_eq!(market, OrderType::Market);
544
545        let limit: OrderType = serde_json::from_str(r#""Limit""#).unwrap();
546        assert_eq!(limit, OrderType::Limit);
547    }
548
549    #[test]
550    fn test_time_in_force_serialization() {
551        let gtc_json = serde_json::to_string(&TimeInForce::GTC).unwrap();
552        assert_eq!(gtc_json, r#""GTC""#);
553
554        let ioc_json = serde_json::to_string(&TimeInForce::IOC).unwrap();
555        assert_eq!(ioc_json, r#""IOC""#);
556
557        let fok_json = serde_json::to_string(&TimeInForce::FOK).unwrap();
558        assert_eq!(fok_json, r#""FOK""#);
559    }
560
561    #[test]
562    fn test_order_status_serialization() {
563        let new_json = serde_json::to_string(&OrderStatus::New).unwrap();
564        assert_eq!(new_json, r#""New""#);
565
566        let filled_json = serde_json::to_string(&OrderStatus::Filled).unwrap();
567        assert_eq!(filled_json, r#""Filled""#);
568
569        let cancelled_json = serde_json::to_string(&OrderStatus::Cancelled).unwrap();
570        assert_eq!(cancelled_json, r#""Cancelled""#);
571    }
572
573    #[test]
574    fn test_server_time_serialization() {
575        let time = ServerTime {
576            time_second: "1234567890".to_string(),
577            time_nano: "1234567890123456789".to_string(),
578        };
579
580        let json = serde_json::to_string(&time).unwrap();
581        assert!(json.contains("\"timeSecond\":\"1234567890\""));
582        assert!(json.contains("\"timeNano\":\"1234567890123456789\""));
583    }
584
585    #[test]
586    fn test_server_time_deserialization() {
587        let json = r#"{"timeSecond":"1234567890","timeNano":"1234567890123456789"}"#;
588        let time: ServerTime = serde_json::from_str(json).unwrap();
589        assert_eq!(time.time_second, "1234567890");
590        assert_eq!(time.time_nano, "1234567890123456789");
591    }
592
593    #[test]
594    fn test_ticker_list_serialization() {
595        let ticker_list = TickerList {
596            list: vec![],
597            next_page_cursor: None,
598        };
599
600        let json = serde_json::to_string(&ticker_list).unwrap();
601        assert!(json.contains("\"list\":[]"));
602    }
603
604    #[test]
605    fn test_create_order_request_default() {
606        let request = CreateOrderRequest {
607            category: "linear".to_string(),
608            symbol: "BTCUSDT".to_string(),
609            side: "Buy".to_string(),
610            order_type: "Limit".to_string(),
611            ..Default::default()
612        };
613
614        assert_eq!(request.category, "linear");
615        assert_eq!(request.symbol, "BTCUSDT");
616        assert_eq!(request.side, "Buy");
617        assert_eq!(request.order_type, "Limit");
618        assert!(request.qty.is_none());
619        assert!(request.price.is_none());
620    }
621
622    #[test]
623    fn test_create_order_request_with_all_fields() {
624        let request = CreateOrderRequest {
625            category: "linear".to_string(),
626            symbol: "BTCUSDT".to_string(),
627            side: "Buy".to_string(),
628            order_type: "Limit".to_string(),
629            qty: Some("0.001".to_string()),
630            price: Some("28000".to_string()),
631            time_in_force: Some("GTC".to_string()),
632            reduce_only: Some(false),
633            take_profit: Some("30000".to_string()),
634            stop_loss: Some("27000".to_string()),
635            ..Default::default()
636        };
637
638        let json = serde_json::to_string(&request).unwrap();
639        assert!(json.contains("\"category\":\"linear\""));
640        assert!(json.contains("\"symbol\":\"BTCUSDT\""));
641        assert!(json.contains("\"qty\":\"0.001\""));
642        assert!(json.contains("\"price\":\"28000\""));
643        assert!(json.contains("\"reduceOnly\":false"));
644    }
645
646    #[test]
647    fn test_create_order_request_builder_basic() {
648        let request = CreateOrderRequest::builder()
649            .category("linear")
650            .symbol("BTCUSDT")
651            .side("Buy")
652            .order_type("Limit")
653            .build();
654
655        assert_eq!(request.category, "linear");
656        assert_eq!(request.symbol, "BTCUSDT");
657        assert_eq!(request.side, "Buy");
658        assert_eq!(request.order_type, "Limit");
659    }
660
661    #[test]
662    #[should_panic(expected = "symbol is required")]
663    fn test_create_order_request_builder_missing_symbol() {
664        let _ = CreateOrderRequest::builder()
665            .category("linear")
666            .side("Buy")
667            .order_type("Limit")
668            .build();
669    }
670
671    #[test]
672    #[should_panic(expected = "side is required")]
673    fn test_create_order_request_builder_missing_side() {
674        let _ = CreateOrderRequest::builder()
675            .category("linear")
676            .symbol("BTCUSDT")
677            .order_type("Limit")
678            .build();
679    }
680
681    #[test]
682    #[should_panic(expected = "order_type is required")]
683    fn test_create_order_request_builder_missing_order_type() {
684        let _ = CreateOrderRequest::builder()
685            .category("linear")
686            .symbol("BTCUSDT")
687            .side("Buy")
688            .build();
689    }
690
691    #[test]
692    fn test_create_order_request_builder_with_optional_fields() {
693        let request = CreateOrderRequest::builder()
694            .category("linear")
695            .symbol("BTCUSDT")
696            .side("Buy")
697            .order_type("Limit")
698            .qty("0.001")
699            .price("28000")
700            .time_in_force("GTC")
701            .position_idx(1)
702            .order_link_id("my_order")
703            .take_profit("30000")
704            .stop_loss("27000")
705            .reduce_only(false)
706            .close_on_trigger(false)
707            .build();
708
709        assert_eq!(request.qty, Some("0.001".to_string()));
710        assert_eq!(request.price, Some("28000".to_string()));
711        assert_eq!(request.time_in_force, Some("GTC".to_string()));
712        assert_eq!(request.position_idx, Some(1));
713        assert_eq!(request.order_link_id, Some("my_order".to_string()));
714        assert_eq!(request.take_profit, Some("30000".to_string()));
715        assert_eq!(request.stop_loss, Some("27000".to_string()));
716        assert_eq!(request.reduce_only, Some(false));
717        assert_eq!(request.close_on_trigger, Some(false));
718    }
719
720    #[test]
721    fn test_create_order_request_builder_default_category() {
722        let request = CreateOrderRequest::builder()
723            .symbol("BTCUSDT")
724            .side("Buy")
725            .order_type("Limit")
726            .build();
727
728        assert_eq!(request.category, "linear");
729    }
730
731    #[test]
732    fn test_create_order_request_builder_chaining() {
733        let request = CreateOrderRequest::builder()
734            .symbol("BTCUSDT")
735            .side("Buy")
736            .order_type("Limit")
737            .qty("0.001")
738            .price("28000")
739            .time_in_force("GTC")
740            .build();
741
742        assert_eq!(request.symbol, "BTCUSDT");
743        assert_eq!(request.side, "Buy");
744        assert_eq!(request.order_type, "Limit");
745        assert_eq!(request.qty, Some("0.001".to_string()));
746        assert_eq!(request.price, Some("28000".to_string()));
747        assert_eq!(request.time_in_force, Some("GTC".to_string()));
748    }
749
750    #[test]
751    fn test_create_order_request_optional_fields_skipped_in_json() {
752        let request = CreateOrderRequest::builder()
753            .symbol("BTCUSDT")
754            .side("Buy")
755            .order_type("Market")
756            .build();
757
758        let json = serde_json::to_string(&request).unwrap();
759        assert!(json.contains("\"symbol\":\"BTCUSDT\""));
760        assert!(json.contains("\"side\":\"Buy\""));
761        assert!(json.contains("\"orderType\":\"Market\""));
762        assert!(!json.contains("\"price\""));
763        assert!(!json.contains("\"qty\""));
764    }
765}