Skip to main content

nautilus_okx/python/
config.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 for OKX configuration.
17
18use nautilus_common::python::config_error_to_pyvalue_err;
19use nautilus_core::string::secret::SecretString;
20use nautilus_model::{enums::OmsType, identifiers::AccountId};
21use nautilus_network::websocket::TransportBackend;
22use pyo3::prelude::*;
23
24use crate::{
25    common::enums::{OKXEnvironment, OKXInstrumentType, OKXMarginMode, OKXRegion, OKXVipLevel},
26    config::{OKXDataClientConfig, OKXExecutionClientConfig},
27};
28
29#[pymethods]
30#[pyo3_stub_gen::derive::gen_stub_pymethods]
31impl OKXDataClientConfig {
32    /// Configuration for the OKX data client.
33    #[new]
34    #[pyo3(signature = (
35        instrument_types = None,
36        environment = None,
37        region = None,
38        api_key = None,
39        api_secret = None,
40        api_passphrase = None,
41        base_url_http = None,
42        base_url_ws_public = None,
43        base_url_ws_business = None,
44        proxy_url = None,
45        http_timeout_secs = None,
46        max_retries = None,
47        retry_delay_initial_ms = None,
48        retry_delay_max_ms = None,
49        update_instruments_interval_mins = None,
50        book_stale_check_interval_secs = None,
51        book_stale_threshold_secs = None,
52        book_snapshot_timeout_secs = None,
53        vip_level = None,
54        load_spreads = false,
55        transport_backend = None,
56        instrument_families = None,
57    ))]
58    #[expect(clippy::too_many_arguments)]
59    fn py_new(
60        instrument_types: Option<Vec<OKXInstrumentType>>,
61        environment: Option<OKXEnvironment>,
62        region: Option<OKXRegion>,
63        api_key: Option<String>,
64        api_secret: Option<String>,
65        api_passphrase: Option<String>,
66        base_url_http: Option<String>,
67        base_url_ws_public: Option<String>,
68        base_url_ws_business: Option<String>,
69        proxy_url: Option<String>,
70        http_timeout_secs: Option<u64>,
71        max_retries: Option<u32>,
72        retry_delay_initial_ms: Option<u64>,
73        retry_delay_max_ms: Option<u64>,
74        update_instruments_interval_mins: Option<u64>,
75        book_stale_check_interval_secs: Option<u64>,
76        book_stale_threshold_secs: Option<u64>,
77        book_snapshot_timeout_secs: Option<u64>,
78        vip_level: Option<OKXVipLevel>,
79        load_spreads: bool,
80        transport_backend: Option<TransportBackend>,
81        instrument_families: Option<Vec<String>>,
82    ) -> PyResult<Self> {
83        let defaults = Self::default();
84
85        let config = Self {
86            api_key: api_key.map(SecretString::from),
87            api_secret: api_secret.map(SecretString::from),
88            api_passphrase: api_passphrase.map(SecretString::from),
89            instrument_types: instrument_types.unwrap_or(defaults.instrument_types),
90            contract_types: None,
91            load_spreads,
92            instrument_families,
93            base_url_http,
94            base_url_ws_public,
95            base_url_ws_business,
96            proxy_url: proxy_url.map(SecretString::from),
97            environment: environment.unwrap_or(defaults.environment),
98            region: region.unwrap_or(defaults.region),
99            http_timeout_secs: http_timeout_secs.unwrap_or(defaults.http_timeout_secs),
100            max_retries: max_retries.unwrap_or(defaults.max_retries),
101            retry_delay_initial_ms: retry_delay_initial_ms
102                .unwrap_or(defaults.retry_delay_initial_ms),
103            retry_delay_max_ms: retry_delay_max_ms.unwrap_or(defaults.retry_delay_max_ms),
104            update_instruments_interval_mins: update_instruments_interval_mins
105                .unwrap_or(defaults.update_instruments_interval_mins),
106            book_stale_check_interval_secs: book_stale_check_interval_secs
107                .unwrap_or(defaults.book_stale_check_interval_secs),
108            book_stale_threshold_secs: book_stale_threshold_secs
109                .unwrap_or(defaults.book_stale_threshold_secs),
110            book_snapshot_timeout_secs: book_snapshot_timeout_secs
111                .unwrap_or(defaults.book_snapshot_timeout_secs),
112            vip_level,
113            transport_backend: transport_backend.unwrap_or(defaults.transport_backend),
114        };
115
116        config.validate().map_err(config_error_to_pyvalue_err)?;
117        Ok(config)
118    }
119
120    #[getter]
121    const fn has_proxy_url(&self) -> bool {
122        self.proxy_url.is_some()
123    }
124
125    fn __repr__(&self) -> String {
126        stringify!(OKXDataClientConfig).to_string()
127    }
128}
129
130#[pymethods]
131#[pyo3_stub_gen::derive::gen_stub_pymethods]
132impl OKXExecutionClientConfig {
133    /// Configuration for the OKX execution client.
134    #[new]
135    #[pyo3(signature = (
136        account_id,
137        instrument_types = None,
138        environment = None,
139        region = None,
140        api_key = None,
141        api_secret = None,
142        api_passphrase = None,
143        base_url_http = None,
144        base_url_ws_private = None,
145        base_url_ws_business = None,
146        proxy_url = None,
147        http_timeout_secs = None,
148        max_retries = None,
149        retry_delay_initial_ms = None,
150        retry_delay_max_ms = None,
151        margin_mode = None,
152        load_spreads = false,
153        auth_timeout_secs = None,
154        transport_backend = None,
155        spot_trade_quote_ccy = None,
156        oms_type = None,
157        max_reconciliation_history_pages = None,
158    ))]
159    #[expect(clippy::too_many_arguments)]
160    fn py_new(
161        account_id: AccountId,
162        instrument_types: Option<Vec<OKXInstrumentType>>,
163        environment: Option<OKXEnvironment>,
164        region: Option<OKXRegion>,
165        api_key: Option<String>,
166        api_secret: Option<String>,
167        api_passphrase: Option<String>,
168        base_url_http: Option<String>,
169        base_url_ws_private: Option<String>,
170        base_url_ws_business: Option<String>,
171        proxy_url: Option<String>,
172        http_timeout_secs: Option<u64>,
173        max_retries: Option<u32>,
174        retry_delay_initial_ms: Option<u64>,
175        retry_delay_max_ms: Option<u64>,
176        margin_mode: Option<OKXMarginMode>,
177        load_spreads: bool,
178        auth_timeout_secs: Option<u64>,
179        transport_backend: Option<TransportBackend>,
180        spot_trade_quote_ccy: Option<String>,
181        oms_type: Option<OmsType>,
182        max_reconciliation_history_pages: Option<u32>,
183    ) -> PyResult<Self> {
184        let defaults = Self::default();
185
186        let config = Self {
187            account_id,
188            oms_type,
189            api_key: api_key.map(SecretString::from),
190            api_secret: api_secret.map(SecretString::from),
191            api_passphrase: api_passphrase.map(SecretString::from),
192            instrument_types: instrument_types.unwrap_or(defaults.instrument_types),
193            contract_types: None,
194            instrument_families: None,
195            base_url_http,
196            base_url_ws_private,
197            base_url_ws_business,
198            proxy_url: proxy_url.map(SecretString::from),
199            environment: environment.unwrap_or(defaults.environment),
200            region: region.unwrap_or(defaults.region),
201            http_timeout_secs: http_timeout_secs.unwrap_or(defaults.http_timeout_secs),
202            use_mm_mass_cancel: defaults.use_mm_mass_cancel,
203            max_retries: max_retries.unwrap_or(defaults.max_retries),
204            retry_delay_initial_ms: retry_delay_initial_ms
205                .unwrap_or(defaults.retry_delay_initial_ms),
206            retry_delay_max_ms: retry_delay_max_ms.unwrap_or(defaults.retry_delay_max_ms),
207            margin_mode,
208            load_spreads,
209            use_spot_margin: defaults.use_spot_margin,
210            auth_timeout_secs,
211            transport_backend: transport_backend.unwrap_or(defaults.transport_backend),
212            spot_trade_quote_ccy,
213            max_reconciliation_history_pages,
214        };
215
216        config.validate().map_err(config_error_to_pyvalue_err)?;
217        Ok(config)
218    }
219
220    #[getter]
221    const fn has_proxy_url(&self) -> bool {
222        self.proxy_url.is_some()
223    }
224
225    fn __repr__(&self) -> String {
226        stringify!(OKXExecutionClientConfig).to_string()
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use pyo3::{
233        Python,
234        exceptions::PyValueError,
235        types::{PyDict, PyList},
236    };
237    use rstest::rstest;
238
239    use super::*;
240
241    #[rstest]
242    #[case::data(false)]
243    #[case::execution(true)]
244    fn test_python_config_duplicate_types(
245        #[case] execution: bool,
246        #[values(false, true)] duplicate: bool,
247    ) {
248        Python::initialize();
249        Python::attach(|py| {
250            let values = if duplicate {
251                vec![
252                    OKXInstrumentType::Swap,
253                    OKXInstrumentType::Spot,
254                    OKXInstrumentType::Swap,
255                ]
256            } else {
257                vec![OKXInstrumentType::Swap, OKXInstrumentType::Spot]
258            };
259
260            let objects: Vec<_> = values
261                .iter()
262                .map(|value| Py::new(py, *value).unwrap())
263                .collect();
264            let list = PyList::new(py, objects).unwrap();
265            let kwargs = PyDict::new(py);
266            kwargs.set_item("instrument_types", list).unwrap();
267            if execution {
268                kwargs
269                    .set_item(
270                        "account_id",
271                        Py::new(py, AccountId::from("OKX-001")).unwrap(),
272                    )
273                    .unwrap();
274            }
275
276            let config_type = if execution {
277                py.get_type::<OKXExecutionClientConfig>()
278            } else {
279                py.get_type::<OKXDataClientConfig>()
280            };
281
282            let result = config_type.call((), Some(&kwargs));
283            if duplicate {
284                let error = result.unwrap_err();
285                assert!(error.is_instance_of::<PyValueError>(py));
286                assert_eq!(
287                    error.value(py).str().unwrap().to_str().unwrap(),
288                    "duplicate instrument_types"
289                );
290            } else {
291                let config = result.unwrap();
292                let actual: Vec<OKXInstrumentType> = config
293                    .getattr("instrument_types")
294                    .unwrap()
295                    .extract()
296                    .unwrap();
297                assert_eq!(actual, values);
298            }
299        });
300    }
301
302    #[rstest]
303    fn test_data_config_py_new() {
304        let config = OKXDataClientConfig::py_new(
305            None,
306            None,
307            None,
308            None,
309            None,
310            None,
311            None,
312            None,
313            None,
314            None,
315            None,
316            None,
317            None,
318            None,
319            None,
320            None,
321            None,
322            None,
323            None,
324            true,
325            None,
326            Some(vec!["BTC-USD".to_string()]),
327        )
328        .unwrap();
329
330        assert_eq!(
331            config.instrument_families,
332            Some(vec!["BTC-USD".to_string()]),
333        );
334        assert!(config.load_spreads);
335        assert_eq!(config.book_stale_check_interval_secs, 5);
336        assert_eq!(config.book_stale_threshold_secs, 30);
337        assert_eq!(config.book_snapshot_timeout_secs, 10);
338    }
339
340    #[rstest]
341    #[case(None)]
342    #[case(Some(0))]
343    #[case(Some(2))]
344    #[case(Some(51))]
345    #[case(Some(u32::MAX))]
346    fn test_exec_config_python_history_pages(#[case] pages: Option<u32>) {
347        Python::initialize();
348        Python::attach(|py| {
349            let kwargs = PyDict::new(py);
350            kwargs
351                .set_item(
352                    "account_id",
353                    Py::new(py, AccountId::from("OKX-001")).unwrap(),
354                )
355                .unwrap();
356
357            if let Some(pages) = pages {
358                kwargs
359                    .set_item("max_reconciliation_history_pages", pages)
360                    .unwrap();
361            }
362
363            let config = py
364                .get_type::<OKXExecutionClientConfig>()
365                .call((), Some(&kwargs))
366                .unwrap();
367            let actual: Option<u32> = config
368                .getattr("max_reconciliation_history_pages")
369                .unwrap()
370                .extract()
371                .unwrap();
372
373            assert_eq!(actual, pages);
374        });
375    }
376
377    #[rstest]
378    fn test_exec_config_py_new_load_spreads() {
379        let config = OKXExecutionClientConfig::py_new(
380            AccountId::from("OKX-001"),
381            None,
382            None,
383            None,
384            None,
385            None,
386            None,
387            None,
388            None,
389            None,
390            None,
391            None,
392            None,
393            None,
394            None,
395            None,
396            true,
397            None,
398            None,
399            Some("USD".to_string()),
400            Some(OmsType::Hedging),
401            Some(7),
402        )
403        .unwrap();
404
405        assert!(config.load_spreads);
406        assert_eq!(config.auth_timeout_secs, None);
407        assert_eq!(config.spot_trade_quote_ccy.as_deref(), Some("USD"));
408        assert_eq!(config.oms_type, Some(OmsType::Hedging));
409        assert_eq!(config.max_reconciliation_history_pages, Some(7));
410    }
411}