Skip to main content

nautilus_bybit/python/
mod.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
16//! Python bindings from `pyo3`.
17
18pub mod config;
19pub mod enums;
20pub mod factories;
21pub mod http;
22pub mod params;
23pub mod types;
24pub mod urls;
25pub mod websocket;
26
27use nautilus_common::factories::{ClientConfig, DataClientFactory, ExecutionClientFactory};
28use nautilus_core::python::{to_pyruntime_err, to_pyvalue_err};
29use nautilus_model::enums::{BarAggregation, OrderSide};
30use nautilus_system::get_global_pyo3_registry;
31use pyo3::prelude::*;
32
33use crate::{
34    common::{
35        consts::BYBIT_NAUTILUS_BROKER_ID,
36        enums::{BybitOrderSide, BybitPositionIdx, BybitPositionMode},
37        parse::{bar_spec_to_bybit_interval, extract_raw_symbol},
38        symbol::BybitSymbol,
39    },
40    config::{BybitDataClientConfig, BybitExecClientConfig},
41    execution::resolve_position_idx,
42    factories::{BybitDataClientFactory, BybitExecutionClientFactory},
43};
44
45/// Extracts the raw symbol from a Bybit symbol by removing the product type suffix.
46///
47/// # Examples
48/// - `"ETHUSDT-LINEAR"` → `"ETHUSDT"`
49/// - `"BTCUSDT-SPOT"` → `"BTCUSDT"`
50/// - `"ETHUSDT"` → `"ETHUSDT"` (no suffix)
51#[pyfunction]
52#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.bybit")]
53#[pyo3(name = "bybit_extract_raw_symbol")]
54fn py_bybit_extract_raw_symbol(symbol: &str) -> &str {
55    extract_raw_symbol(symbol)
56}
57
58/// Converts a Nautilus bar aggregation and step to a Bybit kline interval string.
59///
60/// # Errors
61///
62/// Returns an error if the aggregation type or step is not supported by Bybit.
63#[pyfunction]
64#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.bybit")]
65#[pyo3(name = "bybit_bar_spec_to_interval")]
66fn py_bybit_bar_spec_to_interval(aggregation: u8, step: u64) -> PyResult<String> {
67    let aggregation = BarAggregation::from_repr(aggregation as usize)
68        .ok_or_else(|| to_pyvalue_err(format!("Invalid BarAggregation value: {aggregation}")))?;
69    let interval = bar_spec_to_bybit_interval(aggregation, step).map_err(to_pyvalue_err)?;
70    Ok(interval.to_string())
71}
72
73/// Extracts the product type from a Bybit symbol.
74///
75/// # Examples
76/// - `"ETHUSDT-LINEAR"` → `BybitProductType.LINEAR`
77/// - `"BTCUSDT-SPOT"` → `BybitProductType.SPOT`
78/// - `"BTCUSD-INVERSE"` → `BybitProductType.INVERSE`
79/// - `"ETH-26JUN26-16000-P-OPTION"` → `BybitProductType.OPTION`
80///
81/// # Errors
82///
83/// Returns an error if the symbol does not contain a valid Bybit product type suffix.
84#[pyfunction]
85#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.bybit")]
86#[pyo3(name = "bybit_product_type_from_symbol")]
87fn py_bybit_product_type_from_symbol(
88    symbol: &str,
89) -> PyResult<crate::common::enums::BybitProductType> {
90    let bybit_symbol = BybitSymbol::new(symbol).map_err(to_pyvalue_err)?;
91    Ok(bybit_symbol.product_type())
92}
93
94/// Resolves the Bybit `positionIdx` for an outgoing order.
95///
96/// Returns `None` when no position mode is configured. Otherwise returns the
97/// hedge-mode index for the position being affected (long or short), accounting
98/// for `is_reduce_only`. A `manual_override` always wins.
99#[pyfunction]
100#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.bybit")]
101#[pyo3(name = "bybit_resolve_position_idx")]
102#[pyo3(signature = (position_mode, order_side, is_reduce_only, manual_override=None))]
103fn py_bybit_resolve_position_idx(
104    position_mode: Option<BybitPositionMode>,
105    order_side: OrderSide,
106    is_reduce_only: bool,
107    manual_override: Option<BybitPositionIdx>,
108) -> PyResult<Option<BybitPositionIdx>> {
109    let bybit_side = BybitOrderSide::try_from(order_side).map_err(to_pyvalue_err)?;
110    Ok(resolve_position_idx(
111        position_mode,
112        bybit_side,
113        is_reduce_only,
114        manual_override,
115    ))
116}
117
118#[expect(clippy::needless_pass_by_value)]
119fn extract_bybit_data_factory(
120    py: Python<'_>,
121    factory: Py<PyAny>,
122) -> PyResult<Box<dyn DataClientFactory>> {
123    match factory.extract::<BybitDataClientFactory>(py) {
124        Ok(f) => Ok(Box::new(f)),
125        Err(e) => Err(to_pyvalue_err(format!(
126            "Failed to extract BybitDataClientFactory: {e}"
127        ))),
128    }
129}
130
131#[expect(clippy::needless_pass_by_value)]
132fn extract_bybit_exec_factory(
133    py: Python<'_>,
134    factory: Py<PyAny>,
135) -> PyResult<Box<dyn ExecutionClientFactory>> {
136    match factory.extract::<BybitExecutionClientFactory>(py) {
137        Ok(f) => Ok(Box::new(f)),
138        Err(e) => Err(to_pyvalue_err(format!(
139            "Failed to extract BybitExecutionClientFactory: {e}"
140        ))),
141    }
142}
143
144#[expect(clippy::needless_pass_by_value)]
145fn extract_bybit_data_config(py: Python<'_>, config: Py<PyAny>) -> PyResult<Box<dyn ClientConfig>> {
146    match config.extract::<BybitDataClientConfig>(py) {
147        Ok(c) => Ok(Box::new(c)),
148        Err(e) => Err(to_pyvalue_err(format!(
149            "Failed to extract BybitDataClientConfig: {e}"
150        ))),
151    }
152}
153
154#[expect(clippy::needless_pass_by_value)]
155fn extract_bybit_exec_config(py: Python<'_>, config: Py<PyAny>) -> PyResult<Box<dyn ClientConfig>> {
156    match config.extract::<BybitExecClientConfig>(py) {
157        Ok(c) => Ok(Box::new(c)),
158        Err(e) => Err(to_pyvalue_err(format!(
159            "Failed to extract BybitExecClientConfig: {e}"
160        ))),
161    }
162}
163
164/// Loaded as `nautilus_pyo3.bybit`.
165///
166/// # Errors
167///
168/// Returns an error if any bindings fail to register with the Python module.
169#[pymodule]
170#[rustfmt::skip]
171pub fn bybit(_: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
172    m.add(stringify!(BYBIT_NAUTILUS_BROKER_ID), BYBIT_NAUTILUS_BROKER_ID)?;
173    m.add_class::<crate::common::enums::BybitAccountType>()?;
174    m.add_class::<crate::common::enums::BybitCancelType>()?;
175    m.add_class::<crate::common::enums::BybitEnvironment>()?;
176    m.add_class::<crate::common::enums::BybitMarginAction>()?;
177    m.add_class::<crate::common::enums::BybitMarginMode>()?;
178    m.add_class::<crate::common::enums::BybitOpenOnly>()?;
179    m.add_class::<crate::common::enums::BybitOrderFilter>()?;
180    m.add_class::<crate::common::enums::BybitOrderSide>()?;
181    m.add_class::<crate::common::enums::BybitOrderStatus>()?;
182    m.add_class::<crate::common::enums::BybitOrderType>()?;
183    m.add_class::<crate::common::enums::BybitPositionIdx>()?;
184    m.add_class::<crate::common::enums::BybitPositionMode>()?;
185    m.add_class::<crate::common::enums::BybitProductType>()?;
186    m.add_class::<crate::common::enums::BybitStopOrderType>()?;
187    m.add_class::<crate::common::enums::BybitTimeInForce>()?;
188    m.add_class::<crate::common::enums::BybitTpSlMode>()?;
189    m.add_class::<crate::common::enums::BybitTriggerDirection>()?;
190    m.add_class::<crate::common::enums::BybitTriggerType>()?;
191    m.add_class::<crate::http::client::BybitHttpClient>()?;
192    m.add_class::<crate::http::client::BybitRawHttpClient>()?;
193    m.add_class::<crate::http::models::BybitServerTime>()?;
194    m.add_class::<crate::http::models::BybitOrder>()?;
195    m.add_class::<crate::http::models::BybitOrderCursorList>()?;
196    m.add_class::<crate::http::models::BybitTickerData>()?;
197    m.add_class::<crate::common::types::BybitMarginBorrowResult>()?;
198    m.add_class::<crate::common::types::BybitMarginRepayResult>()?;
199    m.add_class::<crate::common::types::BybitMarginStatusResult>()?;
200    m.add_class::<crate::websocket::client::BybitWebSocketClient>()?;
201    m.add_class::<crate::websocket::messages::BybitWebSocketError>()?;
202    m.add_class::<params::BybitWsPlaceOrderParams>()?;
203    m.add_class::<params::BybitWsAmendOrderParams>()?;
204    m.add_class::<params::BybitWsCancelOrderParams>()?;
205    m.add_class::<params::BybitTickersParams>()?;
206    m.add_class::<BybitDataClientConfig>()?;
207    m.add_class::<BybitExecClientConfig>()?;
208    m.add_class::<BybitDataClientFactory>()?;
209    m.add_class::<BybitExecutionClientFactory>()?;
210    m.add_function(wrap_pyfunction!(urls::py_get_bybit_http_base_url, m)?)?;
211    m.add_function(wrap_pyfunction!(urls::py_get_bybit_ws_url_public, m)?)?;
212    m.add_function(wrap_pyfunction!(urls::py_get_bybit_ws_url_private, m)?)?;
213    m.add_function(wrap_pyfunction!(urls::py_get_bybit_ws_url_trade, m)?)?;
214    m.add_function(wrap_pyfunction!(py_bybit_extract_raw_symbol, m)?)?;
215    m.add_function(wrap_pyfunction!(py_bybit_bar_spec_to_interval, m)?)?;
216    m.add_function(wrap_pyfunction!(py_bybit_product_type_from_symbol, m)?)?;
217    m.add_function(wrap_pyfunction!(py_bybit_resolve_position_idx, m)?)?;
218
219    let registry = get_global_pyo3_registry();
220
221    if let Err(e) =
222        registry.register_factory_extractor("BYBIT".to_string(), extract_bybit_data_factory)
223    {
224        return Err(to_pyruntime_err(format!(
225            "Failed to register Bybit data factory extractor: {e}"
226        )));
227    }
228
229    if let Err(e) = registry
230        .register_exec_factory_extractor("BYBIT".to_string(), extract_bybit_exec_factory)
231    {
232        return Err(to_pyruntime_err(format!(
233            "Failed to register Bybit exec factory extractor: {e}"
234        )));
235    }
236
237    if let Err(e) = registry.register_config_extractor(
238        "BybitDataClientConfig".to_string(),
239        extract_bybit_data_config,
240    ) {
241        return Err(to_pyruntime_err(format!(
242            "Failed to register Bybit data config extractor: {e}"
243        )));
244    }
245
246    if let Err(e) = registry.register_config_extractor(
247        "BybitExecClientConfig".to_string(),
248        extract_bybit_exec_config,
249    ) {
250        return Err(to_pyruntime_err(format!(
251            "Failed to register Bybit exec config extractor: {e}"
252        )));
253    }
254
255    Ok(())
256}