Skip to main content

nautilus_bybit/python/
params.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use pyo3::prelude::*;
17use serde_json;
18use ustr::Ustr;
19
20use crate::{
21    common::{
22        enums::{
23            BybitBboSideType, BybitMarketUnit, BybitOrderSide, BybitOrderType, BybitPositionIdx,
24            BybitProductType, BybitTimeInForce, BybitTriggerType,
25        },
26        parse::{parse_bbo_level, parse_tp_sl_order_type, parse_tpsl_mode, parse_trigger_type},
27    },
28    http::query::BybitNativeTpSlParams as RustNativeTpSlParams,
29    websocket::{error::BybitWsError, messages},
30};
31
32/// Parameters for placing an order via WebSocket.
33#[pyclass(from_py_object)]
34#[pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.bybit")]
35#[derive(Clone, Debug)]
36pub struct BybitWsPlaceOrderParams {
37    #[pyo3(get, set)]
38    pub category: BybitProductType,
39    #[pyo3(get, set)]
40    pub symbol: String,
41    #[pyo3(get, set)]
42    pub side: String,
43    #[pyo3(get, set)]
44    pub order_type: String,
45    #[pyo3(get, set)]
46    pub qty: String,
47    #[pyo3(get, set)]
48    pub is_leverage: Option<i32>,
49    #[pyo3(get, set)]
50    pub market_unit: Option<String>,
51    #[pyo3(get, set)]
52    pub price: Option<String>,
53    #[pyo3(get, set)]
54    pub time_in_force: Option<String>,
55    #[pyo3(get, set)]
56    pub order_link_id: Option<String>,
57    #[pyo3(get, set)]
58    pub reduce_only: Option<bool>,
59    #[pyo3(get, set)]
60    pub close_on_trigger: Option<bool>,
61    #[pyo3(get, set)]
62    pub trigger_price: Option<String>,
63    #[pyo3(get, set)]
64    pub trigger_by: Option<String>,
65    #[pyo3(get, set)]
66    pub trigger_direction: Option<i32>,
67    #[pyo3(get, set)]
68    pub tpsl_mode: Option<String>,
69    #[pyo3(get, set)]
70    pub take_profit: Option<String>,
71    #[pyo3(get, set)]
72    pub stop_loss: Option<String>,
73    #[pyo3(get, set)]
74    pub tp_trigger_by: Option<String>,
75    #[pyo3(get, set)]
76    pub sl_trigger_by: Option<String>,
77    #[pyo3(get, set)]
78    pub sl_trigger_price: Option<String>,
79    #[pyo3(get, set)]
80    pub tp_trigger_price: Option<String>,
81    #[pyo3(get, set)]
82    pub sl_order_type: Option<String>,
83    #[pyo3(get, set)]
84    pub tp_order_type: Option<String>,
85    #[pyo3(get, set)]
86    pub sl_limit_price: Option<String>,
87    #[pyo3(get, set)]
88    pub tp_limit_price: Option<String>,
89    #[pyo3(get, set)]
90    pub order_iv: Option<String>,
91    #[pyo3(get, set)]
92    pub mmp: Option<bool>,
93    #[pyo3(get, set)]
94    pub position_idx: Option<BybitPositionIdx>,
95    #[pyo3(get, set)]
96    pub bbo_side_type: Option<String>,
97    #[pyo3(get, set)]
98    pub bbo_level: Option<String>,
99}
100
101#[pymethods]
102#[pyo3_stub_gen::derive::gen_stub_pymethods]
103impl BybitWsPlaceOrderParams {
104    /// Parameters for placing an order via WebSocket.
105    #[new]
106    #[pyo3(signature = (
107        category,
108        symbol,
109        side,
110        order_type,
111        qty,
112        is_leverage=None,
113        market_unit=None,
114        price=None,
115        time_in_force=None,
116        order_link_id=None,
117        reduce_only=None,
118        close_on_trigger=None,
119        trigger_price=None,
120        trigger_by=None,
121        trigger_direction=None,
122        tpsl_mode=None,
123        take_profit=None,
124        stop_loss=None,
125        tp_trigger_by=None,
126        sl_trigger_by=None,
127        sl_trigger_price=None,
128        tp_trigger_price=None,
129        sl_order_type=None,
130        tp_order_type=None,
131        sl_limit_price=None,
132        tp_limit_price=None,
133        order_iv=None,
134        mmp=None,
135        position_idx=None,
136        bbo_side_type=None,
137        bbo_level=None,
138    ))]
139    #[expect(clippy::too_many_arguments)]
140    fn py_new(
141        category: BybitProductType,
142        symbol: String,
143        side: String,
144        order_type: String,
145        qty: String,
146        is_leverage: Option<i32>,
147        market_unit: Option<String>,
148        price: Option<String>,
149        time_in_force: Option<String>,
150        order_link_id: Option<String>,
151        reduce_only: Option<bool>,
152        close_on_trigger: Option<bool>,
153        trigger_price: Option<String>,
154        trigger_by: Option<String>,
155        trigger_direction: Option<i32>,
156        tpsl_mode: Option<String>,
157        take_profit: Option<String>,
158        stop_loss: Option<String>,
159        tp_trigger_by: Option<String>,
160        sl_trigger_by: Option<String>,
161        sl_trigger_price: Option<String>,
162        tp_trigger_price: Option<String>,
163        sl_order_type: Option<String>,
164        tp_order_type: Option<String>,
165        sl_limit_price: Option<String>,
166        tp_limit_price: Option<String>,
167        order_iv: Option<String>,
168        mmp: Option<bool>,
169        position_idx: Option<BybitPositionIdx>,
170        bbo_side_type: Option<String>,
171        bbo_level: Option<String>,
172    ) -> Self {
173        Self {
174            category,
175            symbol,
176            side,
177            order_type,
178            qty,
179            is_leverage,
180            market_unit,
181            price,
182            time_in_force,
183            order_link_id,
184            reduce_only,
185            close_on_trigger,
186            trigger_price,
187            trigger_by,
188            trigger_direction,
189            tpsl_mode,
190            take_profit,
191            stop_loss,
192            tp_trigger_by,
193            sl_trigger_by,
194            sl_trigger_price,
195            tp_trigger_price,
196            sl_order_type,
197            tp_order_type,
198            sl_limit_price,
199            tp_limit_price,
200            order_iv,
201            mmp,
202            position_idx,
203            bbo_side_type,
204            bbo_level,
205        }
206    }
207}
208
209impl TryFrom<BybitWsPlaceOrderParams> for messages::BybitWsPlaceOrderParams {
210    type Error = BybitWsError;
211
212    fn try_from(params: BybitWsPlaceOrderParams) -> Result<Self, Self::Error> {
213        let side: BybitOrderSide =
214            serde_json::from_str(&format!("\"{}\"", params.side)).map_err(|e| {
215                BybitWsError::ClientError(format!("Invalid side '{}': {}", params.side, e))
216            })?;
217        let order_type: BybitOrderType =
218            serde_json::from_str(&format!("\"{}\"", params.order_type)).map_err(|e| {
219                BybitWsError::ClientError(format!(
220                    "Invalid order_type '{}': {}",
221                    params.order_type, e
222                ))
223            })?;
224
225        let time_in_force = params
226            .time_in_force
227            .map(|v| {
228                serde_json::from_str::<BybitTimeInForce>(&format!("\"{v}\"")).map_err(|e| {
229                    BybitWsError::ClientError(format!("Invalid time_in_force '{v}': {e}"))
230                })
231            })
232            .transpose()?;
233
234        let trigger_by = params
235            .trigger_by
236            .map(|v| {
237                serde_json::from_str::<BybitTriggerType>(&format!("\"{v}\"")).map_err(|e| {
238                    BybitWsError::ClientError(format!("Invalid trigger_by '{v}': {e}"))
239                })
240            })
241            .transpose()?;
242
243        let tp_trigger_by = params
244            .tp_trigger_by
245            .map(|v| {
246                serde_json::from_str::<BybitTriggerType>(&format!("\"{v}\"")).map_err(|e| {
247                    BybitWsError::ClientError(format!("Invalid tp_trigger_by '{v}': {e}"))
248                })
249            })
250            .transpose()?;
251
252        let sl_trigger_by = params
253            .sl_trigger_by
254            .map(|v| {
255                serde_json::from_str::<BybitTriggerType>(&format!("\"{v}\"")).map_err(|e| {
256                    BybitWsError::ClientError(format!("Invalid sl_trigger_by '{v}': {e}"))
257                })
258            })
259            .transpose()?;
260
261        let sl_order_type = params
262            .sl_order_type
263            .map(|v| {
264                serde_json::from_str::<BybitOrderType>(&format!("\"{v}\"")).map_err(|e| {
265                    BybitWsError::ClientError(format!("Invalid sl_order_type '{v}': {e}"))
266                })
267            })
268            .transpose()?;
269
270        let tp_order_type = params
271            .tp_order_type
272            .map(|v| {
273                serde_json::from_str::<BybitOrderType>(&format!("\"{v}\"")).map_err(|e| {
274                    BybitWsError::ClientError(format!("Invalid tp_order_type '{v}': {e}"))
275                })
276            })
277            .transpose()?;
278
279        let tpsl_mode = params
280            .tpsl_mode
281            .map(|v| {
282                parse_tpsl_mode(&v)
283                    .map_err(|e| BybitWsError::ClientError(format!("Invalid tpsl_mode '{v}': {e}")))
284            })
285            .transpose()?;
286
287        let market_unit = params
288            .market_unit
289            .map(|v| {
290                serde_json::from_str::<BybitMarketUnit>(&format!("\"{v}\"")).map_err(|e| {
291                    BybitWsError::ClientError(format!("Invalid market_unit '{v}': {e}"))
292                })
293            })
294            .transpose()?;
295
296        let bbo_side_type = params
297            .bbo_side_type
298            .map(|v| {
299                serde_json::from_str::<BybitBboSideType>(&format!("\"{v}\"")).map_err(|e| {
300                    BybitWsError::ClientError(format!("Invalid bbo_side_type '{v}': {e}"))
301                })
302            })
303            .transpose()?;
304        let bbo_level = params
305            .bbo_level
306            .map(parse_bbo_level)
307            .transpose()
308            .map_err(|e| BybitWsError::ClientError(e.to_string()))?;
309
310        if bbo_side_type.is_some() != bbo_level.is_some() {
311            return Err(BybitWsError::ClientError(
312                "'bbo_side_type' and 'bbo_level' must be provided together".to_string(),
313            ));
314        }
315
316        Ok(Self {
317            category: params.category,
318            symbol: Ustr::from(&params.symbol),
319            side,
320            order_type,
321            qty: params.qty,
322            is_leverage: params.is_leverage,
323            market_unit,
324            price: params.price,
325            time_in_force,
326            order_link_id: params.order_link_id,
327            reduce_only: params.reduce_only,
328            close_on_trigger: params.close_on_trigger,
329            trigger_price: params.trigger_price,
330            trigger_by,
331            trigger_direction: params.trigger_direction,
332            tpsl_mode,
333            take_profit: params.take_profit,
334            stop_loss: params.stop_loss,
335            tp_trigger_by,
336            sl_trigger_by,
337            sl_trigger_price: params.sl_trigger_price,
338            tp_trigger_price: params.tp_trigger_price,
339            sl_order_type,
340            tp_order_type,
341            sl_limit_price: params.sl_limit_price,
342            tp_limit_price: params.tp_limit_price,
343            order_iv: params.order_iv,
344            mmp: params.mmp,
345            position_idx: params.position_idx,
346            bbo_side_type,
347            bbo_level,
348        })
349    }
350}
351
352impl From<messages::BybitWsPlaceOrderParams> for BybitWsPlaceOrderParams {
353    fn from(params: messages::BybitWsPlaceOrderParams) -> Self {
354        let side = serde_json::to_string(&params.side)
355            .expect("Failed to serialize BybitOrderSide")
356            .trim_matches('"')
357            .to_string();
358        let order_type = serde_json::to_string(&params.order_type)
359            .expect("Failed to serialize BybitOrderType")
360            .trim_matches('"')
361            .to_string();
362        let time_in_force = params.time_in_force.map(|v| {
363            serde_json::to_string(&v)
364                .expect("Failed to serialize BybitTimeInForce")
365                .trim_matches('"')
366                .to_string()
367        });
368        let trigger_by = params.trigger_by.map(|v| {
369            serde_json::to_string(&v)
370                .expect("Failed to serialize BybitTriggerType")
371                .trim_matches('"')
372                .to_string()
373        });
374        let tp_trigger_by = params.tp_trigger_by.map(|v| {
375            serde_json::to_string(&v)
376                .expect("Failed to serialize BybitTriggerType")
377                .trim_matches('"')
378                .to_string()
379        });
380        let sl_trigger_by = params.sl_trigger_by.map(|v| {
381            serde_json::to_string(&v)
382                .expect("Failed to serialize BybitTriggerType")
383                .trim_matches('"')
384                .to_string()
385        });
386        let sl_order_type = params.sl_order_type.map(|v| {
387            serde_json::to_string(&v)
388                .expect("Failed to serialize BybitOrderType")
389                .trim_matches('"')
390                .to_string()
391        });
392        let tp_order_type = params.tp_order_type.map(|v| {
393            serde_json::to_string(&v)
394                .expect("Failed to serialize BybitOrderType")
395                .trim_matches('"')
396                .to_string()
397        });
398
399        let tpsl_mode = params.tpsl_mode.map(|v| {
400            serde_json::to_string(&v)
401                .expect("Failed to serialize BybitTpSlMode")
402                .trim_matches('"')
403                .to_string()
404        });
405        let market_unit = params.market_unit.map(|v| {
406            serde_json::to_string(&v)
407                .expect("Failed to serialize BybitMarketUnit")
408                .trim_matches('"')
409                .to_string()
410        });
411        let bbo_side_type = params.bbo_side_type.map(|v| {
412            serde_json::to_string(&v)
413                .expect("Failed to serialize BybitBboSideType")
414                .trim_matches('"')
415                .to_string()
416        });
417
418        Self {
419            category: params.category,
420            symbol: params.symbol.to_string(),
421            side,
422            order_type,
423            qty: params.qty,
424            is_leverage: params.is_leverage,
425            market_unit,
426            price: params.price,
427            time_in_force,
428            order_link_id: params.order_link_id,
429            reduce_only: params.reduce_only,
430            close_on_trigger: params.close_on_trigger,
431            trigger_price: params.trigger_price,
432            trigger_by,
433            trigger_direction: params.trigger_direction,
434            tpsl_mode,
435            take_profit: params.take_profit,
436            stop_loss: params.stop_loss,
437            tp_trigger_by,
438            sl_trigger_by,
439            sl_trigger_price: params.sl_trigger_price,
440            tp_trigger_price: params.tp_trigger_price,
441            sl_order_type,
442            tp_order_type,
443            sl_limit_price: params.sl_limit_price,
444            tp_limit_price: params.tp_limit_price,
445            order_iv: params.order_iv,
446            mmp: params.mmp,
447            position_idx: params.position_idx,
448            bbo_side_type,
449            bbo_level: params.bbo_level,
450        }
451    }
452}
453
454#[cfg(test)]
455mod tests {
456    use rstest::rstest;
457
458    use super::*;
459    use crate::common::enums::BybitTpSlMode;
460
461    fn place_order_params(
462        bbo_side_type: Option<&str>,
463        bbo_level: Option<&str>,
464    ) -> BybitWsPlaceOrderParams {
465        BybitWsPlaceOrderParams {
466            category: BybitProductType::Linear,
467            symbol: "BTCUSDT".to_string(),
468            side: "Buy".to_string(),
469            order_type: "Limit".to_string(),
470            qty: "0.001".to_string(),
471            is_leverage: None,
472            market_unit: None,
473            price: None,
474            time_in_force: Some("GTC".to_string()),
475            order_link_id: Some("test-bbo-1".to_string()),
476            reduce_only: None,
477            close_on_trigger: None,
478            trigger_price: None,
479            trigger_by: None,
480            trigger_direction: None,
481            tpsl_mode: None,
482            take_profit: None,
483            stop_loss: None,
484            tp_trigger_by: None,
485            sl_trigger_by: None,
486            sl_trigger_price: None,
487            tp_trigger_price: None,
488            sl_order_type: None,
489            tp_order_type: None,
490            sl_limit_price: None,
491            tp_limit_price: None,
492            order_iv: None,
493            mmp: None,
494            position_idx: None,
495            bbo_side_type: bbo_side_type.map(str::to_string),
496            bbo_level: bbo_level.map(str::to_string),
497        }
498    }
499
500    #[rstest]
501    fn test_place_order_params_try_from_accepts_bbo_pair() {
502        let params = place_order_params(Some("Queue"), Some("2"));
503        let result = messages::BybitWsPlaceOrderParams::try_from(params).unwrap();
504
505        assert_eq!(result.bbo_side_type, Some(BybitBboSideType::Queue));
506        assert_eq!(result.bbo_level.as_deref(), Some("2"));
507    }
508
509    #[rstest]
510    fn test_place_order_params_try_from_rejects_unpaired_bbo() {
511        let params = place_order_params(Some("Queue"), None);
512        let err = messages::BybitWsPlaceOrderParams::try_from(params).unwrap_err();
513
514        assert!(
515            err.to_string()
516                .contains("'bbo_side_type' and 'bbo_level' must be provided together")
517        );
518    }
519
520    #[rstest]
521    fn test_place_order_params_try_from_rejects_invalid_bbo_level() {
522        let params = place_order_params(Some("Queue"), Some("6"));
523        let err = messages::BybitWsPlaceOrderParams::try_from(params).unwrap_err();
524
525        assert!(err.to_string().contains("invalid 'bbo_level'"));
526    }
527
528    #[rstest]
529    fn test_native_tp_sl_params_try_from_accepts_valid_enums() {
530        let params = BybitNativeTpSlParams {
531            take_profit: Some("55000".to_string()),
532            stop_loss: Some("47000".to_string()),
533            tp_trigger_by: Some("LastPrice".to_string()),
534            sl_trigger_by: Some("MarkPrice".to_string()),
535            tp_order_type: Some("Limit".to_string()),
536            sl_order_type: Some("Market".to_string()),
537            tpsl_mode: Some("Partial".to_string()),
538            ..Default::default()
539        };
540
541        let native = RustNativeTpSlParams::try_from(params).unwrap();
542
543        assert_eq!(native.tp_trigger_by, Some(BybitTriggerType::LastPrice));
544        assert_eq!(native.sl_trigger_by, Some(BybitTriggerType::MarkPrice));
545        assert_eq!(native.tp_order_type, Some(BybitOrderType::Limit));
546        assert_eq!(native.sl_order_type, Some(BybitOrderType::Market));
547        assert_eq!(native.tpsl_mode, Some(BybitTpSlMode::Partial));
548    }
549
550    #[rstest]
551    #[case("tp_trigger_by")]
552    #[case("sl_trigger_by")]
553    #[case("tp_order_type")]
554    #[case("sl_order_type")]
555    #[case("tpsl_mode")]
556    fn test_native_tp_sl_params_try_from_rejects_invalid_enum(#[case] field: &str) {
557        let mut params = BybitNativeTpSlParams::default();
558        match field {
559            "tp_trigger_by" => params.tp_trigger_by = Some("garbage".to_string()),
560            "sl_trigger_by" => params.sl_trigger_by = Some("garbage".to_string()),
561            "tp_order_type" => params.tp_order_type = Some("garbage".to_string()),
562            "sl_order_type" => params.sl_order_type = Some("garbage".to_string()),
563            "tpsl_mode" => params.tpsl_mode = Some("garbage".to_string()),
564            _ => unreachable!(),
565        }
566
567        let err = RustNativeTpSlParams::try_from(params).unwrap_err();
568        assert!(err.to_string().contains("garbage"));
569    }
570
571    #[rstest]
572    fn test_native_tp_sl_params_try_from_rejects_unknown_tpsl_mode() {
573        // `BybitTpSlMode` has a `#[serde(other)] Unknown` variant; a raw deserialize would
574        // silently accept "Unknown". The validated parser must reject it.
575        let params = BybitNativeTpSlParams {
576            tpsl_mode: Some("Unknown".to_string()),
577            ..Default::default()
578        };
579
580        let err = RustNativeTpSlParams::try_from(params).unwrap_err();
581        assert!(err.to_string().contains("invalid Bybit TP/SL mode"));
582    }
583}
584
585/// Parameters for amending an order via WebSocket.
586#[pyclass(from_py_object)]
587#[pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.bybit")]
588#[derive(Clone, Debug)]
589pub struct BybitWsAmendOrderParams {
590    #[pyo3(get, set)]
591    pub category: BybitProductType,
592    #[pyo3(get, set)]
593    pub symbol: String,
594    #[pyo3(get, set)]
595    pub order_id: Option<String>,
596    #[pyo3(get, set)]
597    pub order_link_id: Option<String>,
598    #[pyo3(get, set)]
599    pub qty: Option<String>,
600    #[pyo3(get, set)]
601    pub price: Option<String>,
602    #[pyo3(get, set)]
603    pub trigger_price: Option<String>,
604    #[pyo3(get, set)]
605    pub take_profit: Option<String>,
606    #[pyo3(get, set)]
607    pub stop_loss: Option<String>,
608    #[pyo3(get, set)]
609    pub tp_trigger_by: Option<String>,
610    #[pyo3(get, set)]
611    pub sl_trigger_by: Option<String>,
612    #[pyo3(get, set)]
613    pub order_iv: Option<String>,
614}
615
616#[pymethods]
617#[pyo3_stub_gen::derive::gen_stub_pymethods]
618impl BybitWsAmendOrderParams {
619    /// Parameters for amending an order via WebSocket.
620    #[new]
621    #[expect(clippy::too_many_arguments)]
622    fn py_new(
623        category: BybitProductType,
624        symbol: String,
625        order_id: Option<String>,
626        order_link_id: Option<String>,
627        qty: Option<String>,
628        price: Option<String>,
629        trigger_price: Option<String>,
630        take_profit: Option<String>,
631        stop_loss: Option<String>,
632        tp_trigger_by: Option<String>,
633        sl_trigger_by: Option<String>,
634        order_iv: Option<String>,
635    ) -> Self {
636        Self {
637            category,
638            symbol,
639            order_id,
640            order_link_id,
641            qty,
642            price,
643            trigger_price,
644            take_profit,
645            stop_loss,
646            tp_trigger_by,
647            sl_trigger_by,
648            order_iv,
649        }
650    }
651}
652
653impl TryFrom<BybitWsAmendOrderParams> for messages::BybitWsAmendOrderParams {
654    type Error = BybitWsError;
655
656    fn try_from(params: BybitWsAmendOrderParams) -> Result<Self, Self::Error> {
657        let tp_trigger_by = params
658            .tp_trigger_by
659            .map(|v| {
660                serde_json::from_str::<BybitTriggerType>(&format!("\"{v}\"")).map_err(|e| {
661                    BybitWsError::ClientError(format!("Invalid tp_trigger_by '{v}': {e}"))
662                })
663            })
664            .transpose()?;
665
666        let sl_trigger_by = params
667            .sl_trigger_by
668            .map(|v| {
669                serde_json::from_str::<BybitTriggerType>(&format!("\"{v}\"")).map_err(|e| {
670                    BybitWsError::ClientError(format!("Invalid sl_trigger_by '{v}': {e}"))
671                })
672            })
673            .transpose()?;
674
675        Ok(Self {
676            category: params.category,
677            symbol: Ustr::from(&params.symbol),
678            order_id: params.order_id,
679            order_link_id: params.order_link_id,
680            qty: params.qty,
681            price: params.price,
682            trigger_price: params.trigger_price,
683            take_profit: params.take_profit,
684            stop_loss: params.stop_loss,
685            tp_trigger_by,
686            sl_trigger_by,
687            order_iv: params.order_iv,
688        })
689    }
690}
691
692impl From<messages::BybitWsAmendOrderParams> for BybitWsAmendOrderParams {
693    fn from(params: messages::BybitWsAmendOrderParams) -> Self {
694        let tp_trigger_by = params.tp_trigger_by.map(|v| {
695            serde_json::to_string(&v)
696                .expect("Failed to serialize BybitTriggerType")
697                .trim_matches('"')
698                .to_string()
699        });
700        let sl_trigger_by = params.sl_trigger_by.map(|v| {
701            serde_json::to_string(&v)
702                .expect("Failed to serialize BybitTriggerType")
703                .trim_matches('"')
704                .to_string()
705        });
706
707        Self {
708            category: params.category,
709            symbol: params.symbol.to_string(),
710            order_id: params.order_id,
711            order_link_id: params.order_link_id,
712            qty: params.qty,
713            price: params.price,
714            trigger_price: params.trigger_price,
715            take_profit: params.take_profit,
716            stop_loss: params.stop_loss,
717            tp_trigger_by,
718            sl_trigger_by,
719            order_iv: params.order_iv,
720        }
721    }
722}
723
724/// Parameters for canceling an order via WebSocket.
725#[pyclass(from_py_object)]
726#[pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.bybit")]
727#[derive(Clone, Debug)]
728pub struct BybitWsCancelOrderParams {
729    #[pyo3(get, set)]
730    pub category: BybitProductType,
731    #[pyo3(get, set)]
732    pub symbol: String,
733    #[pyo3(get, set)]
734    pub order_id: Option<String>,
735    #[pyo3(get, set)]
736    pub order_link_id: Option<String>,
737}
738
739#[pymethods]
740#[pyo3_stub_gen::derive::gen_stub_pymethods]
741impl BybitWsCancelOrderParams {
742    /// Parameters for canceling an order via WebSocket.
743    #[new]
744    fn py_new(
745        category: BybitProductType,
746        symbol: String,
747        order_id: Option<String>,
748        order_link_id: Option<String>,
749    ) -> Self {
750        Self {
751            category,
752            symbol,
753            order_id,
754            order_link_id,
755        }
756    }
757}
758
759impl TryFrom<BybitWsCancelOrderParams> for messages::BybitWsCancelOrderParams {
760    type Error = BybitWsError;
761
762    fn try_from(params: BybitWsCancelOrderParams) -> Result<Self, Self::Error> {
763        Ok(Self {
764            category: params.category,
765            symbol: Ustr::from(&params.symbol),
766            order_id: params.order_id,
767            order_link_id: params.order_link_id,
768        })
769    }
770}
771
772impl From<messages::BybitWsCancelOrderParams> for BybitWsCancelOrderParams {
773    fn from(params: messages::BybitWsCancelOrderParams) -> Self {
774        Self {
775            category: params.category,
776            symbol: params.symbol.to_string(),
777            order_id: params.order_id,
778            order_link_id: params.order_link_id,
779        }
780    }
781}
782
783/// Parameters for fetching tickers via HTTP API.
784#[pyclass(from_py_object)]
785#[pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.bybit")]
786#[derive(Clone, Debug)]
787pub struct BybitTickersParams {
788    #[pyo3(get, set)]
789    pub category: BybitProductType,
790    #[pyo3(get, set)]
791    pub symbol: Option<String>,
792    #[pyo3(get, set)]
793    pub base_coin: Option<String>,
794    #[pyo3(get, set)]
795    pub exp_date: Option<String>,
796}
797
798#[pymethods]
799#[pyo3_stub_gen::derive::gen_stub_pymethods]
800impl BybitTickersParams {
801    /// Query parameters for `GET /v5/market/tickers`.
802    ///
803    /// # References
804    /// - <https://bybit-exchange.github.io/docs/v5/market/tickers>
805    #[new]
806    #[pyo3(signature = (category, symbol=None, base_coin=None, exp_date=None))]
807    fn py_new(
808        category: BybitProductType,
809        symbol: Option<String>,
810        base_coin: Option<String>,
811        exp_date: Option<String>,
812    ) -> Self {
813        Self {
814            category,
815            symbol,
816            base_coin,
817            exp_date,
818        }
819    }
820}
821
822impl From<BybitTickersParams> for crate::http::query::BybitTickersParams {
823    fn from(params: BybitTickersParams) -> Self {
824        Self {
825            category: params.category,
826            symbol: params.symbol,
827            base_coin: params.base_coin,
828            exp_date: params.exp_date,
829        }
830    }
831}
832
833/// Native TP/SL and option-specific fields for `POST /v5/order/create` (used by the demo HTTP
834/// path, since demo does not expose the mainnet WS Trade API).
835///
836/// Enum-typed fields are accepted as strings (matching the existing [`BybitWsPlaceOrderParams`]
837/// surface) and parsed at the binding boundary.
838#[pyclass(from_py_object)]
839#[pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.bybit")]
840#[derive(Debug, Clone, Default)]
841pub struct BybitNativeTpSlParams {
842    #[pyo3(get, set)]
843    pub take_profit: Option<String>,
844    #[pyo3(get, set)]
845    pub stop_loss: Option<String>,
846    #[pyo3(get, set)]
847    pub tp_trigger_by: Option<String>,
848    #[pyo3(get, set)]
849    pub sl_trigger_by: Option<String>,
850    #[pyo3(get, set)]
851    pub tp_order_type: Option<String>,
852    #[pyo3(get, set)]
853    pub sl_order_type: Option<String>,
854    #[pyo3(get, set)]
855    pub tp_limit_price: Option<String>,
856    #[pyo3(get, set)]
857    pub sl_limit_price: Option<String>,
858    #[pyo3(get, set)]
859    pub tpsl_mode: Option<String>,
860    #[pyo3(get, set)]
861    pub close_on_trigger: Option<bool>,
862    #[pyo3(get, set)]
863    pub order_iv: Option<String>,
864    #[pyo3(get, set)]
865    pub mmp: Option<bool>,
866}
867
868#[pymethods]
869#[pyo3_stub_gen::derive::gen_stub_pymethods]
870impl BybitNativeTpSlParams {
871    /// Native TP/SL and option-specific fields that map onto the `POST /v5/order/create` entry.
872    ///
873    /// Bundled to keep the `submit_order` signature manageable, and to give the demo HTTP path
874    /// access to the same fields the mainnet WS path supports via
875    /// `crate.websocket.messages.BybitWsPlaceOrderParams`. All fields are optional; populated
876    /// fields are written onto the entry builder as-is. `tpsl_mode` defaults to `Full` upstream when
877    /// only `take_profit` / `stop_loss` are set without an explicit mode.
878    ///
879    /// `tp_trigger_price` / `sl_trigger_price` are intentionally absent: the create-order entry does
880    /// not carry them (the mainnet WS Trade API does, via separate fields).
881    #[new]
882    #[pyo3(signature = (
883        take_profit=None,
884        stop_loss=None,
885        tp_trigger_by=None,
886        sl_trigger_by=None,
887        tp_order_type=None,
888        sl_order_type=None,
889        tp_limit_price=None,
890        sl_limit_price=None,
891        tpsl_mode=None,
892        close_on_trigger=None,
893        order_iv=None,
894        mmp=None,
895    ))]
896    #[expect(clippy::too_many_arguments)]
897    fn py_new(
898        take_profit: Option<String>,
899        stop_loss: Option<String>,
900        tp_trigger_by: Option<String>,
901        sl_trigger_by: Option<String>,
902        tp_order_type: Option<String>,
903        sl_order_type: Option<String>,
904        tp_limit_price: Option<String>,
905        sl_limit_price: Option<String>,
906        tpsl_mode: Option<String>,
907        close_on_trigger: Option<bool>,
908        order_iv: Option<String>,
909        mmp: Option<bool>,
910    ) -> Self {
911        Self {
912            take_profit,
913            stop_loss,
914            tp_trigger_by,
915            sl_trigger_by,
916            tp_order_type,
917            sl_order_type,
918            tp_limit_price,
919            sl_limit_price,
920            tpsl_mode,
921            close_on_trigger,
922            order_iv,
923            mmp,
924        }
925    }
926}
927
928impl TryFrom<BybitNativeTpSlParams> for RustNativeTpSlParams {
929    type Error = anyhow::Error;
930
931    fn try_from(params: BybitNativeTpSlParams) -> anyhow::Result<Self> {
932        Ok(Self {
933            take_profit: params.take_profit,
934            stop_loss: params.stop_loss,
935            tp_trigger_by: params
936                .tp_trigger_by
937                .as_deref()
938                .map(parse_trigger_type)
939                .transpose()?,
940            sl_trigger_by: params
941                .sl_trigger_by
942                .as_deref()
943                .map(parse_trigger_type)
944                .transpose()?,
945            tp_order_type: params
946                .tp_order_type
947                .as_deref()
948                .map(parse_tp_sl_order_type)
949                .transpose()?,
950            sl_order_type: params
951                .sl_order_type
952                .as_deref()
953                .map(parse_tp_sl_order_type)
954                .transpose()?,
955            tp_limit_price: params.tp_limit_price,
956            sl_limit_price: params.sl_limit_price,
957            tpsl_mode: params
958                .tpsl_mode
959                .as_deref()
960                .map(parse_tpsl_mode)
961                .transpose()?,
962            close_on_trigger: params.close_on_trigger,
963            order_iv: params.order_iv,
964            mmp: params.mmp,
965        })
966    }
967}