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::*;
17
18use crate::{
19    common::{
20        enums::BybitProductType,
21        parse::{parse_tp_sl_order_type, parse_tpsl_mode, parse_trigger_type},
22    },
23    http::query::BybitNativeTpSlParams as RustNativeTpSlParams,
24};
25
26#[cfg(test)]
27mod tests {
28    use rstest::rstest;
29
30    use super::*;
31    use crate::common::enums::{BybitOrderType, BybitTpSlMode, BybitTriggerType};
32
33    #[rstest]
34    fn test_native_tp_sl_params_try_from_accepts_valid_enums() {
35        let params = BybitNativeTpSlParams {
36            take_profit: Some("55000".to_string()),
37            stop_loss: Some("47000".to_string()),
38            tp_trigger_by: Some("LastPrice".to_string()),
39            sl_trigger_by: Some("MarkPrice".to_string()),
40            tp_order_type: Some("Limit".to_string()),
41            sl_order_type: Some("Market".to_string()),
42            tpsl_mode: Some("Partial".to_string()),
43            ..Default::default()
44        };
45
46        let native = RustNativeTpSlParams::try_from(params).unwrap();
47
48        assert_eq!(native.tp_trigger_by, Some(BybitTriggerType::LastPrice));
49        assert_eq!(native.sl_trigger_by, Some(BybitTriggerType::MarkPrice));
50        assert_eq!(native.tp_order_type, Some(BybitOrderType::Limit));
51        assert_eq!(native.sl_order_type, Some(BybitOrderType::Market));
52        assert_eq!(native.tpsl_mode, Some(BybitTpSlMode::Partial));
53    }
54
55    #[rstest]
56    #[case("tp_trigger_by")]
57    #[case("sl_trigger_by")]
58    #[case("tp_order_type")]
59    #[case("sl_order_type")]
60    #[case("tpsl_mode")]
61    fn test_native_tp_sl_params_try_from_rejects_invalid_enum(#[case] field: &str) {
62        let mut params = BybitNativeTpSlParams::default();
63        match field {
64            "tp_trigger_by" => params.tp_trigger_by = Some("garbage".to_string()),
65            "sl_trigger_by" => params.sl_trigger_by = Some("garbage".to_string()),
66            "tp_order_type" => params.tp_order_type = Some("garbage".to_string()),
67            "sl_order_type" => params.sl_order_type = Some("garbage".to_string()),
68            "tpsl_mode" => params.tpsl_mode = Some("garbage".to_string()),
69            _ => unreachable!(),
70        }
71
72        let err = RustNativeTpSlParams::try_from(params).unwrap_err();
73        assert!(err.to_string().contains("garbage"));
74    }
75
76    #[rstest]
77    fn test_native_tp_sl_params_try_from_rejects_unknown_tpsl_mode() {
78        // `BybitTpSlMode` has a `#[serde(other)] Unknown` variant; a raw deserialize would
79        // silently accept "Unknown". The validated parser must reject it.
80        let params = BybitNativeTpSlParams {
81            tpsl_mode: Some("Unknown".to_string()),
82            ..Default::default()
83        };
84
85        let err = RustNativeTpSlParams::try_from(params).unwrap_err();
86        assert!(err.to_string().contains("invalid Bybit TP/SL mode"));
87    }
88}
89
90/// Parameters for fetching tickers via HTTP API.
91#[pyclass(from_py_object)]
92#[pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.bybit")]
93#[derive(Clone, Debug)]
94pub struct BybitTickersParams {
95    #[pyo3(get, set)]
96    pub category: BybitProductType,
97    #[pyo3(get, set)]
98    pub symbol: Option<String>,
99    #[pyo3(get, set)]
100    pub base_coin: Option<String>,
101    #[pyo3(get, set)]
102    pub exp_date: Option<String>,
103}
104
105#[pymethods]
106#[pyo3_stub_gen::derive::gen_stub_pymethods]
107impl BybitTickersParams {
108    /// Query parameters for `GET /v5/market/tickers`.
109    ///
110    /// # References
111    /// - <https://bybit-exchange.github.io/docs/v5/market/tickers>
112    #[new]
113    #[pyo3(signature = (category, symbol=None, base_coin=None, exp_date=None))]
114    fn py_new(
115        category: BybitProductType,
116        symbol: Option<String>,
117        base_coin: Option<String>,
118        exp_date: Option<String>,
119    ) -> Self {
120        Self {
121            category,
122            symbol,
123            base_coin,
124            exp_date,
125        }
126    }
127}
128
129impl From<BybitTickersParams> for crate::http::query::BybitTickersParams {
130    fn from(params: BybitTickersParams) -> Self {
131        Self {
132            category: params.category,
133            symbol: params.symbol,
134            base_coin: params.base_coin,
135            exp_date: params.exp_date,
136        }
137    }
138}
139
140/// Native TP/SL and option-specific fields for `POST /v5/order/create` (used by the demo HTTP
141/// path, since demo does not expose the mainnet WS Trade API).
142///
143/// Enum-typed fields are accepted as strings and parsed at the binding boundary.
144#[pyclass(from_py_object)]
145#[pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.bybit")]
146#[derive(Debug, Clone, Default)]
147pub struct BybitNativeTpSlParams {
148    #[pyo3(get, set)]
149    pub take_profit: Option<String>,
150    #[pyo3(get, set)]
151    pub stop_loss: Option<String>,
152    #[pyo3(get, set)]
153    pub tp_trigger_by: Option<String>,
154    #[pyo3(get, set)]
155    pub sl_trigger_by: Option<String>,
156    #[pyo3(get, set)]
157    pub tp_order_type: Option<String>,
158    #[pyo3(get, set)]
159    pub sl_order_type: Option<String>,
160    #[pyo3(get, set)]
161    pub tp_limit_price: Option<String>,
162    #[pyo3(get, set)]
163    pub sl_limit_price: Option<String>,
164    #[pyo3(get, set)]
165    pub tpsl_mode: Option<String>,
166    #[pyo3(get, set)]
167    pub close_on_trigger: Option<bool>,
168    #[pyo3(get, set)]
169    pub order_iv: Option<String>,
170    #[pyo3(get, set)]
171    pub mmp: Option<bool>,
172}
173
174#[pymethods]
175#[pyo3_stub_gen::derive::gen_stub_pymethods]
176impl BybitNativeTpSlParams {
177    /// Native TP/SL and option-specific fields that map onto the `POST /v5/order/create` entry.
178    ///
179    /// Bundled to keep the `submit_order` signature manageable, and to give the demo HTTP path
180    /// access to the same fields the mainnet WS path supports via
181    /// `crate.websocket.messages.BybitWsPlaceOrderParams`. All fields are optional; populated
182    /// fields are written onto the entry builder as-is. `tpsl_mode` defaults to `Full` upstream when
183    /// only `take_profit` / `stop_loss` are set without an explicit mode.
184    ///
185    /// `tp_trigger_price` / `sl_trigger_price` are intentionally absent: the create-order entry does
186    /// not carry them (the mainnet WS Trade API does, via separate fields).
187    #[new]
188    #[pyo3(signature = (
189        take_profit=None,
190        stop_loss=None,
191        tp_trigger_by=None,
192        sl_trigger_by=None,
193        tp_order_type=None,
194        sl_order_type=None,
195        tp_limit_price=None,
196        sl_limit_price=None,
197        tpsl_mode=None,
198        close_on_trigger=None,
199        order_iv=None,
200        mmp=None,
201    ))]
202    #[expect(clippy::too_many_arguments)]
203    fn py_new(
204        take_profit: Option<String>,
205        stop_loss: Option<String>,
206        tp_trigger_by: Option<String>,
207        sl_trigger_by: Option<String>,
208        tp_order_type: Option<String>,
209        sl_order_type: Option<String>,
210        tp_limit_price: Option<String>,
211        sl_limit_price: Option<String>,
212        tpsl_mode: Option<String>,
213        close_on_trigger: Option<bool>,
214        order_iv: Option<String>,
215        mmp: Option<bool>,
216    ) -> Self {
217        Self {
218            take_profit,
219            stop_loss,
220            tp_trigger_by,
221            sl_trigger_by,
222            tp_order_type,
223            sl_order_type,
224            tp_limit_price,
225            sl_limit_price,
226            tpsl_mode,
227            close_on_trigger,
228            order_iv,
229            mmp,
230        }
231    }
232}
233
234impl TryFrom<BybitNativeTpSlParams> for RustNativeTpSlParams {
235    type Error = anyhow::Error;
236
237    fn try_from(params: BybitNativeTpSlParams) -> anyhow::Result<Self> {
238        Ok(Self {
239            take_profit: params.take_profit,
240            stop_loss: params.stop_loss,
241            tp_trigger_by: params
242                .tp_trigger_by
243                .as_deref()
244                .map(parse_trigger_type)
245                .transpose()?,
246            sl_trigger_by: params
247                .sl_trigger_by
248                .as_deref()
249                .map(parse_trigger_type)
250                .transpose()?,
251            tp_order_type: params
252                .tp_order_type
253                .as_deref()
254                .map(parse_tp_sl_order_type)
255                .transpose()?,
256            sl_order_type: params
257                .sl_order_type
258                .as_deref()
259                .map(parse_tp_sl_order_type)
260                .transpose()?,
261            tp_limit_price: params.tp_limit_price,
262            sl_limit_price: params.sl_limit_price,
263            tpsl_mode: params
264                .tpsl_mode
265                .as_deref()
266                .map(parse_tpsl_mode)
267                .transpose()?,
268            close_on_trigger: params.close_on_trigger,
269            order_iv: params.order_iv,
270            mmp: params.mmp,
271        })
272    }
273}