Skip to main content

nautilus_bybit/
factories.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//! Factory functions for creating Bybit clients and components.
17
18use std::{any::Any, cell::RefCell, rc::Rc};
19
20use nautilus_common::{
21    cache::CacheView,
22    clients::{DataClient, ExecutionClient},
23    clock::Clock,
24    factories::{ClientConfig, DataClientFactory, ExecutionClientFactory},
25};
26use nautilus_live::ExecutionClientCore;
27use nautilus_model::{
28    enums::{AccountType, OmsType},
29    identifiers::{AccountId, ClientId, TraderId},
30};
31
32use crate::{
33    common::{
34        consts::{BYBIT, BYBIT_VENUE},
35        enums::BybitProductType,
36    },
37    config::{BybitDataClientConfig, BybitExecClientConfig},
38    data::BybitDataClient,
39    execution::BybitExecutionClient,
40};
41
42impl ClientConfig for BybitDataClientConfig {
43    fn as_any(&self) -> &dyn Any {
44        self
45    }
46}
47
48impl ClientConfig for BybitExecClientConfig {
49    fn as_any(&self) -> &dyn Any {
50        self
51    }
52}
53
54/// Factory for creating Bybit data clients.
55#[derive(Debug, Clone)]
56#[cfg_attr(
57    feature = "python",
58    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.bybit", from_py_object)
59)]
60#[cfg_attr(
61    feature = "python",
62    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.bybit")
63)]
64pub struct BybitDataClientFactory;
65
66impl BybitDataClientFactory {
67    /// Creates a new [`BybitDataClientFactory`] instance.
68    #[must_use]
69    pub const fn new() -> Self {
70        Self
71    }
72}
73
74impl Default for BybitDataClientFactory {
75    fn default() -> Self {
76        Self::new()
77    }
78}
79
80impl DataClientFactory for BybitDataClientFactory {
81    fn create(
82        &self,
83        name: &str,
84        config: &dyn ClientConfig,
85        _cache: CacheView,
86        _clock: Rc<RefCell<dyn Clock>>,
87    ) -> anyhow::Result<Box<dyn DataClient>> {
88        let bybit_config = config
89            .as_any()
90            .downcast_ref::<BybitDataClientConfig>()
91            .ok_or_else(|| {
92                anyhow::anyhow!(
93                    "Invalid config type for BybitDataClientFactory. Expected BybitDataClientConfig, was {config:?}",
94                )
95            })?
96            .clone();
97
98        let client_id = ClientId::from(name);
99        let client = BybitDataClient::new(client_id, bybit_config)?;
100        Ok(Box::new(client))
101    }
102
103    fn name(&self) -> &'static str {
104        BYBIT
105    }
106
107    fn config_type(&self) -> &'static str {
108        "BybitDataClientConfig"
109    }
110}
111
112/// Factory for creating Bybit execution clients.
113#[derive(Debug, Clone)]
114#[cfg_attr(
115    feature = "python",
116    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.bybit", from_py_object)
117)]
118#[cfg_attr(
119    feature = "python",
120    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.bybit")
121)]
122pub struct BybitExecutionClientFactory {
123    trader_id: TraderId,
124    account_id: AccountId,
125}
126
127impl BybitExecutionClientFactory {
128    /// Creates a new [`BybitExecutionClientFactory`] instance.
129    #[must_use]
130    pub const fn new(trader_id: TraderId, account_id: AccountId) -> Self {
131        Self {
132            trader_id,
133            account_id,
134        }
135    }
136}
137
138impl ExecutionClientFactory for BybitExecutionClientFactory {
139    fn create(
140        &self,
141        name: &str,
142        config: &dyn ClientConfig,
143        cache: CacheView,
144    ) -> anyhow::Result<Box<dyn ExecutionClient>> {
145        let bybit_config = config
146            .as_any()
147            .downcast_ref::<BybitExecClientConfig>()
148            .ok_or_else(|| {
149                anyhow::anyhow!(
150                    "Invalid config type for BybitExecutionClientFactory. Expected BybitExecClientConfig, was {config:?}",
151                )
152            })?
153            .clone();
154
155        // Default to Linear if product_types is empty (matches execution client behavior)
156        let product_types = if bybit_config.product_types.is_empty() {
157            vec![BybitProductType::Linear]
158        } else {
159            bybit_config.product_types.clone()
160        };
161
162        let has_derivatives = product_types.iter().any(|t| {
163            matches!(
164                t,
165                BybitProductType::Linear | BybitProductType::Inverse | BybitProductType::Option
166            )
167        });
168
169        let account_type = if has_derivatives {
170            AccountType::Margin
171        } else {
172            AccountType::Cash
173        };
174
175        // Bybit uses netting for derivatives, hedging for spot
176        let oms_type = if has_derivatives {
177            OmsType::Netting
178        } else {
179            OmsType::Hedging
180        };
181
182        let account_id = bybit_config.account_id.unwrap_or(self.account_id);
183
184        let core = ExecutionClientCore::new(
185            self.trader_id,
186            ClientId::from(name),
187            *BYBIT_VENUE,
188            oms_type,
189            account_id,
190            account_type,
191            None, // base_currency
192            cache,
193        );
194
195        let client = BybitExecutionClient::new(core, bybit_config)?;
196
197        Ok(Box::new(client))
198    }
199
200    fn name(&self) -> &'static str {
201        BYBIT
202    }
203
204    fn config_type(&self) -> &'static str {
205        "BybitExecClientConfig"
206    }
207}
208
209#[cfg(test)]
210mod tests {
211    use std::{cell::RefCell, rc::Rc};
212
213    use nautilus_common::{
214        cache::Cache,
215        factories::{ClientConfig, ExecutionClientFactory},
216    };
217    use nautilus_model::identifiers::{AccountId, TraderId};
218    use rstest::rstest;
219
220    use super::*;
221    use crate::{common::enums::BybitProductType, config::BybitExecClientConfig};
222
223    #[rstest]
224    fn test_bybit_execution_client_factory_creation() {
225        let factory = BybitExecutionClientFactory::new(
226            TraderId::from("TRADER-001"),
227            AccountId::from("BYBIT-001"),
228        );
229        assert_eq!(factory.name(), BYBIT);
230        assert_eq!(factory.config_type(), "BybitExecClientConfig");
231    }
232
233    #[rstest]
234    fn test_bybit_exec_client_config_implements_client_config() {
235        let config = BybitExecClientConfig::default();
236
237        let boxed_config: Box<dyn ClientConfig> = Box::new(config);
238        let downcasted = boxed_config
239            .as_any()
240            .downcast_ref::<BybitExecClientConfig>();
241
242        assert!(downcasted.is_some());
243    }
244
245    #[rstest]
246    fn test_bybit_execution_client_factory_creates_client_for_spot() {
247        let factory = BybitExecutionClientFactory::new(
248            TraderId::from("TRADER-001"),
249            AccountId::from("BYBIT-001"),
250        );
251        let config = BybitExecClientConfig {
252            product_types: vec![BybitProductType::Spot],
253            api_key: Some("test_key".to_string()),
254            api_secret: Some("test_secret".to_string()),
255            ..Default::default()
256        };
257
258        let cache = Rc::new(RefCell::new(Cache::default()));
259
260        let result = factory.create("BYBIT-TEST", &config, cache.into());
261        assert!(result.is_ok());
262
263        let client = result.unwrap();
264        assert_eq!(client.client_id(), ClientId::from("BYBIT-TEST"));
265    }
266
267    #[rstest]
268    fn test_bybit_execution_client_factory_creates_client_for_derivatives() {
269        let factory = BybitExecutionClientFactory::new(
270            TraderId::from("TRADER-001"),
271            AccountId::from("BYBIT-001"),
272        );
273        let config = BybitExecClientConfig {
274            product_types: vec![BybitProductType::Linear, BybitProductType::Inverse],
275            api_key: Some("test_key".to_string()),
276            api_secret: Some("test_secret".to_string()),
277            ..Default::default()
278        };
279
280        let cache = Rc::new(RefCell::new(Cache::default()));
281
282        let result = factory.create("BYBIT-DERIV", &config, cache.into());
283        result.unwrap();
284    }
285
286    #[rstest]
287    fn test_bybit_execution_client_factory_rejects_wrong_config_type() {
288        let factory = BybitExecutionClientFactory::new(
289            TraderId::from("TRADER-001"),
290            AccountId::from("BYBIT-001"),
291        );
292        let wrong_config = BybitDataClientConfig::default();
293
294        let cache = Rc::new(RefCell::new(Cache::default()));
295
296        let result = factory.create("BYBIT-TEST", &wrong_config, cache.into());
297        assert!(result.is_err());
298        assert!(
299            result
300                .err()
301                .unwrap()
302                .to_string()
303                .contains("Invalid config type")
304        );
305    }
306}