1use 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 #[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 #[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}