Skip to main content

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