Skip to main content

nautilus_okx/python/
websocket.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 the OKX WebSocket client.
17//!
18//! # Design Pattern: Clone and Share State
19//!
20//! The WebSocket client must be cloned for async operations because PyO3's `future_into_py`
21//! requires `'static` futures (cannot borrow from `self`). To ensure clones share the same
22//! connection state, key fields use `Arc<RwLock<T>>`:
23//!
24//! - `inner: Arc<RwLock<Option<WebSocketClient>>>` - The WebSocket connection.
25//!
26//! Without shared state, clones would be independent, causing:
27//! - Lost WebSocket messages.
28//! - Missing instrument data.
29//! - Connection state desynchronization.
30//!
31//! ## Connection Flow
32//!
33//! 1. Clone the client for async operation.
34//! 2. Connect and populate shared state on the clone.
35//! 3. Spawn stream handler as background task.
36//! 4. Return immediately (non-blocking).
37//!
38//! ## Important Notes
39//!
40//! - Never use `block_on()` - it blocks the runtime.
41//! - Always clone before async blocks for lifetime requirements.
42//! - RwLock is preferred over Mutex (many reads, few writes).
43
44use std::str::FromStr;
45
46use ahash::{AHashMap, AHashSet};
47use futures_util::StreamExt;
48use nautilus_common::{cache::quote::QuoteCache, live::get_runtime};
49use nautilus_core::{
50    UUID4, UnixNanos,
51    python::{call_python_threadsafe, params::value_to_pyobject, to_pyruntime_err, to_pyvalue_err},
52    time::{AtomicTime, get_atomic_clock_realtime},
53};
54use nautilus_model::{
55    data::{BarType, Data, InstrumentStatus, OrderBookDeltas_API},
56    enums::{OrderSide, OrderType, PositionSide, TimeInForce},
57    events::{OrderAccepted, OrderCancelRejected, OrderModifyRejected, OrderRejected},
58    identifiers::{AccountId, ClientOrderId, InstrumentId, StrategyId, TraderId, VenueOrderId},
59    instruments::{Instrument, InstrumentAny},
60    python::{
61        data::data_to_pycapsule,
62        instruments::{instrument_any_to_pyobject, pyobject_to_instrument_any},
63    },
64    types::{Money, Price, Quantity},
65};
66use nautilus_network::websocket::TransportBackend;
67use pyo3::{
68    IntoPyObjectExt,
69    prelude::*,
70    types::{PyDict, PyTuple},
71};
72use ustr::Ustr;
73
74use super::{extract_optional_string, extract_optional_trigger_type};
75use crate::{
76    common::{
77        consts::{OKX_FIELD_CLORDID, OKX_FIELD_SCODE, OKX_FIELD_SMSG, OKX_SUCCESS_CODE},
78        enums::{
79            OKXBookAction, OKXGreeksType, OKXInstrumentStatus, OKXInstrumentType, OKXTradeMode,
80            OKXVipLevel,
81        },
82        models::OKXInstrument,
83        parse::{
84            okx_status_to_market_action, parse_account_state, parse_instrument_any,
85            parse_instrument_id, parse_millisecond_timestamp, parse_position_status_report,
86            parse_price, parse_quantity,
87        },
88    },
89    http::models::{OKXAccount, OKXPosition, OKXSpreadOrder},
90    websocket::{
91        OKXWebSocketClient,
92        enums::{OKXWsChannel, OKXWsOperation},
93        messages::{
94            ExecutionReport, NautilusWsMessage, OKXAlgoOrderMsg, OKXBookMsg, OKXOptionSummaryMsg,
95            OKXOrderMsg, OKXWebSocketError, OKXWsMessage, WsAttachAlgoOrdParams,
96            WsAttachAlgoOrdParamsBuilder,
97        },
98        parse::{
99            extract_fees_from_cached_instrument, parse_algo_order_msg, parse_book_msg_vec,
100            parse_index_price_msg_vec, parse_option_summary_greeks, parse_order_msg_vec,
101            parse_spread_order_msg, parse_ws_message_data,
102        },
103    },
104};
105
106type PyBatchSubmitOrder = (
107    OKXInstrumentType,
108    InstrumentId,
109    OKXTradeMode,
110    ClientOrderId,
111    OrderSide,
112    OrderType,
113    Quantity,
114    Option<PositionSide>,
115    Option<Price>,
116    Option<Price>,
117    Option<bool>,
118    Option<bool>,
119    Option<String>,
120    Option<String>,
121);
122
123type PyLegacyBatchSubmitOrder = (
124    OKXInstrumentType,
125    InstrumentId,
126    OKXTradeMode,
127    ClientOrderId,
128    OrderSide,
129    OrderType,
130    Quantity,
131    Option<PositionSide>,
132    Option<Price>,
133    Option<Price>,
134    Option<bool>,
135    Option<bool>,
136);
137
138type PyBatchModifyOrder = (
139    String,
140    InstrumentId,
141    ClientOrderId,
142    ClientOrderId,
143    Option<Price>,
144    Option<Quantity>,
145    Option<String>,
146);
147
148type PyLegacyBatchModifyOrder = (
149    String,
150    InstrumentId,
151    ClientOrderId,
152    ClientOrderId,
153    Option<Price>,
154    Option<Quantity>,
155);
156
157fn extract_batch_submit_order(py: Python<'_>, obj: &Py<PyAny>) -> PyResult<PyBatchSubmitOrder> {
158    if let Ok(tuple) = obj.bind(py).cast::<PyTuple>()
159        && tuple.len() == 14
160    {
161        return Ok((
162            tuple.get_item(0)?.extract()?,
163            tuple.get_item(1)?.extract()?,
164            tuple.get_item(2)?.extract()?,
165            tuple.get_item(3)?.extract()?,
166            tuple.get_item(4)?.extract()?,
167            tuple.get_item(5)?.extract()?,
168            tuple.get_item(6)?.extract()?,
169            tuple.get_item(7)?.extract()?,
170            tuple.get_item(8)?.extract()?,
171            tuple.get_item(9)?.extract()?,
172            tuple.get_item(10)?.extract()?,
173            tuple.get_item(11)?.extract()?,
174            tuple.get_item(12)?.extract()?,
175            tuple.get_item(13)?.extract()?,
176        ));
177    }
178
179    let (
180        instrument_type,
181        instrument_id,
182        td_mode,
183        client_order_id,
184        order_side,
185        order_type,
186        quantity,
187        position_side,
188        price,
189        trigger_price,
190        post_only,
191        reduce_only,
192    ): PyLegacyBatchSubmitOrder = obj.extract(py).map_err(to_pyruntime_err)?;
193
194    Ok((
195        instrument_type,
196        instrument_id,
197        td_mode,
198        client_order_id,
199        order_side,
200        order_type,
201        quantity,
202        position_side,
203        price,
204        trigger_price,
205        post_only,
206        reduce_only,
207        None,
208        None,
209    ))
210}
211
212fn parse_attach_algo_ords(
213    py: Python<'_>,
214    attach_algo_ords: Option<Vec<Py<PyDict>>>,
215) -> PyResult<Option<Vec<WsAttachAlgoOrdParams>>> {
216    attach_algo_ords
217        .map(|items| {
218            items
219                .into_iter()
220                .map(|item| {
221                    let dict = item.bind(py);
222                    let mut builder = WsAttachAlgoOrdParamsBuilder::default();
223
224                    if let Some(value) = extract_optional_string(dict, "attach_algo_cl_ord_id")? {
225                        builder.attach_algo_cl_ord_id(value);
226                    }
227
228                    if let Some(value) = extract_optional_string(dict, "sl_trigger_px")? {
229                        builder.sl_trigger_px(value);
230                    }
231
232                    if let Some(value) = extract_optional_string(dict, "sl_ord_px")? {
233                        builder.sl_ord_px(value);
234                    }
235
236                    if let Some(value) = extract_optional_trigger_type(dict, "sl_trigger_px_type")?
237                    {
238                        builder.sl_trigger_px_type(value);
239                    }
240
241                    if let Some(value) = extract_optional_string(dict, "tp_trigger_px")? {
242                        builder.tp_trigger_px(value);
243                    }
244
245                    if let Some(value) = extract_optional_string(dict, "tp_ord_px")? {
246                        builder.tp_ord_px(value);
247                    }
248
249                    if let Some(value) = extract_optional_trigger_type(dict, "tp_trigger_px_type")?
250                    {
251                        builder.tp_trigger_px_type(value);
252                    }
253
254                    if let Some(value) = extract_optional_string(dict, "callback_ratio")? {
255                        builder.callback_ratio(value);
256                    }
257
258                    if let Some(value) = extract_optional_string(dict, "callback_spread")? {
259                        builder.callback_spread(value);
260                    }
261
262                    if let Some(value) = extract_optional_string(dict, "active_px")? {
263                        builder.active_px(value);
264                    }
265
266                    if let Some(value) = extract_optional_string(dict, "new_callback_ratio")? {
267                        builder.new_callback_ratio(value);
268                    }
269
270                    if let Some(value) = extract_optional_string(dict, "new_callback_spread")? {
271                        builder.new_callback_spread(value);
272                    }
273
274                    if let Some(value) = extract_optional_string(dict, "new_active_px")? {
275                        builder.new_active_px(value);
276                    }
277
278                    builder.build().map_err(to_pyvalue_err)
279                })
280                .collect::<PyResult<Vec<_>>>()
281        })
282        .transpose()
283}
284
285#[pyo3::pymethods]
286impl OKXWebSocketError {
287    #[getter]
288    pub fn code(&self) -> &str {
289        &self.code
290    }
291
292    #[getter]
293    pub fn message(&self) -> &str {
294        &self.message
295    }
296
297    #[getter]
298    pub fn conn_id(&self) -> Option<&str> {
299        self.conn_id.as_deref()
300    }
301
302    #[getter]
303    pub fn ts_event(&self) -> u64 {
304        self.timestamp
305    }
306
307    fn __repr__(&self) -> String {
308        format!(
309            "OKXWebSocketError(code='{}', message='{}', conn_id={:?}, ts_event={})",
310            self.code, self.message, self.conn_id, self.timestamp
311        )
312    }
313}
314
315#[pymethods]
316#[pyo3_stub_gen::derive::gen_stub_pymethods]
317impl OKXWebSocketClient {
318    /// Provides a WebSocket client for connecting to [OKX](https://okx.com).
319    #[new]
320    #[pyo3(signature = (url=None, api_key=None, api_secret=None, api_passphrase=None, account_id=None, heartbeat=None, auth_timeout_secs=None, proxy_url=None))]
321    #[expect(clippy::too_many_arguments)]
322    fn py_new(
323        url: Option<String>,
324        api_key: Option<String>,
325        api_secret: Option<String>,
326        api_passphrase: Option<String>,
327        account_id: Option<AccountId>,
328        heartbeat: Option<u64>,
329        auth_timeout_secs: Option<u64>,
330        proxy_url: Option<String>,
331    ) -> PyResult<Self> {
332        Self::new(
333            url,
334            api_key,
335            api_secret,
336            api_passphrase,
337            account_id,
338            heartbeat,
339            auth_timeout_secs,
340            TransportBackend::default(),
341            proxy_url,
342        )
343        .map_err(to_pyvalue_err)
344    }
345
346    #[staticmethod]
347    #[pyo3(name = "with_credentials")]
348    #[pyo3(signature = (url=None, api_key=None, api_secret=None, api_passphrase=None, account_id=None, heartbeat=None, auth_timeout_secs=None, proxy_url=None))]
349    #[expect(clippy::too_many_arguments)]
350    fn py_with_credentials(
351        url: Option<String>,
352        api_key: Option<String>,
353        api_secret: Option<String>,
354        api_passphrase: Option<String>,
355        account_id: Option<AccountId>,
356        heartbeat: Option<u64>,
357        auth_timeout_secs: Option<u64>,
358        proxy_url: Option<String>,
359    ) -> PyResult<Self> {
360        Self::with_credentials(
361            url,
362            api_key,
363            api_secret,
364            api_passphrase,
365            account_id,
366            heartbeat,
367            auth_timeout_secs,
368            TransportBackend::default(),
369            proxy_url,
370        )
371        .map_err(to_pyvalue_err)
372    }
373
374    #[staticmethod]
375    #[pyo3(name = "from_env")]
376    fn py_from_env() -> PyResult<Self> {
377        Self::from_env().map_err(to_pyvalue_err)
378    }
379
380    #[getter]
381    #[pyo3(name = "url")]
382    #[must_use]
383    pub fn py_url(&self) -> &str {
384        self.url()
385    }
386
387    #[getter]
388    #[pyo3(name = "api_key")]
389    #[must_use]
390    pub fn py_api_key(&self) -> Option<&str> {
391        self.api_key()
392    }
393
394    #[getter]
395    #[pyo3(name = "api_key_masked")]
396    #[must_use]
397    pub fn py_api_key_masked(&self) -> Option<String> {
398        self.api_key_masked()
399    }
400
401    #[pyo3(name = "is_active")]
402    fn py_is_active(&mut self) -> bool {
403        self.is_active()
404    }
405
406    #[pyo3(name = "is_closed")]
407    fn py_is_closed(&mut self) -> bool {
408        self.is_closed()
409    }
410
411    #[pyo3(name = "cancel_all_requests")]
412    pub fn py_cancel_all_requests(&self) {
413        self.cancel_all_requests();
414    }
415
416    #[pyo3(name = "get_subscriptions")]
417    fn py_get_subscriptions(&self, instrument_id: InstrumentId) -> Vec<String> {
418        let channels = self.get_subscriptions(instrument_id);
419
420        // Convert to OKX channel names
421        channels
422            .iter()
423            .map(|c| {
424                serde_json::to_value(c)
425                    .ok()
426                    .and_then(|v| v.as_str().map(String::from))
427                    .unwrap_or_else(|| c.to_string())
428            })
429            .collect()
430    }
431
432    /// Sets the VIP level for this client.
433    ///
434    /// The VIP level determines which WebSocket channels are available.
435    #[pyo3(name = "set_vip_level")]
436    fn py_set_vip_level(&self, vip_level: OKXVipLevel) {
437        self.set_vip_level(vip_level);
438    }
439
440    /// Gets the current VIP level.
441    #[pyo3(name = "vip_level")]
442    #[getter]
443    fn py_vip_level(&self) -> OKXVipLevel {
444        self.vip_level()
445    }
446
447    #[pyo3(name = "connect")]
448    #[expect(clippy::needless_pass_by_value)]
449    fn py_connect<'py>(
450        &mut self,
451        py: Python<'py>,
452        loop_: Py<PyAny>,
453        instruments: Vec<Py<PyAny>>,
454        callback: Py<PyAny>,
455    ) -> PyResult<Bound<'py, PyAny>> {
456        let call_soon: Py<PyAny> = loop_.getattr(py, "call_soon_threadsafe")?;
457
458        let mut instruments_any = Vec::new();
459
460        for inst in instruments {
461            let inst_any = pyobject_to_instrument_any(py, inst)?;
462            instruments_any.push(inst_any);
463        }
464
465        self.cache_instruments(&instruments_any);
466
467        let mut client = self.clone();
468
469        pyo3_async_runtimes::tokio::future_into_py(py, async move {
470            client.connect().await.map_err(to_pyruntime_err)?;
471
472            let stream = client.stream();
473            let clock = get_atomic_clock_realtime();
474
475            get_runtime().spawn(async move {
476                let account_id = client.account_id;
477                let mut instruments_by_symbol = client.instruments_snapshot();
478                let mut quote_cache = QuoteCache::new();
479                let mut funding_cache: AHashMap<Ustr, (Ustr, u64)> = AHashMap::new();
480                let mut fee_cache: AHashMap<Ustr, Money> = AHashMap::new();
481                let mut filled_qty_cache: AHashMap<Ustr, Quantity> = AHashMap::new();
482                let option_greeks_subs_arc = client.option_greeks_subs().clone();
483                tokio::pin!(stream);
484
485                while let Some(msg) = stream.next().await {
486                    match msg {
487                        OKXWsMessage::BookData { arg, action, data } => {
488                            handle_book_data(
489                                arg.inst_id,
490                                action,
491                                data,
492                                &instruments_by_symbol,
493                                clock,
494                                &call_soon,
495                                &callback,
496                            );
497                        }
498                        OKXWsMessage::ChannelData {
499                            channel,
500                            inst_id,
501                            data,
502                        } => {
503                            let greeks_guard = option_greeks_subs_arc.load();
504                            handle_channel_data(
505                                &channel,
506                                inst_id,
507                                data,
508                                &mut instruments_by_symbol,
509                                &mut quote_cache,
510                                &mut funding_cache,
511                                &greeks_guard,
512                                clock,
513                                &call_soon,
514                                &callback,
515                            );
516                        }
517                        OKXWsMessage::Instruments(okx_instruments) => {
518                            handle_instruments(
519                                okx_instruments,
520                                &mut instruments_by_symbol,
521                                clock,
522                                &call_soon,
523                                &callback,
524                            );
525                        }
526                        OKXWsMessage::Orders(order_msgs) => {
527                            handle_orders(
528                                &order_msgs,
529                                account_id,
530                                &instruments_by_symbol,
531                                &mut fee_cache,
532                                &mut filled_qty_cache,
533                                clock,
534                                &call_soon,
535                                &callback,
536                            );
537                        }
538                        OKXWsMessage::SpreadOrders(order_msgs) => {
539                            handle_spread_orders(
540                                &order_msgs,
541                                account_id,
542                                &instruments_by_symbol,
543                                &mut filled_qty_cache,
544                                clock,
545                                &call_soon,
546                                &callback,
547                            );
548                        }
549                        OKXWsMessage::AlgoOrders(algo_msgs) => {
550                            handle_algo_orders(
551                                algo_msgs,
552                                account_id,
553                                &instruments_by_symbol,
554                                clock,
555                                &call_soon,
556                                &callback,
557                            );
558                        }
559                        OKXWsMessage::Account(data) => {
560                            handle_account(data, account_id, clock, &call_soon, &callback);
561                        }
562                        OKXWsMessage::Positions(data) => {
563                            handle_positions(
564                                data,
565                                account_id,
566                                &instruments_by_symbol,
567                                clock,
568                                &call_soon,
569                                &callback,
570                            );
571                        }
572                        OKXWsMessage::OrderResponse {
573                            id,
574                            op,
575                            code,
576                            msg,
577                            data,
578                        } => {
579                            handle_order_response(
580                                id.as_deref(),
581                                &op,
582                                &code,
583                                &msg,
584                                &data,
585                                &client,
586                                account_id,
587                                clock,
588                                &call_soon,
589                                &callback,
590                            );
591                        }
592                        OKXWsMessage::SendFailed {
593                            request_id,
594                            client_order_id,
595                            op,
596                            error,
597                        } => {
598                            handle_send_failed(
599                                &request_id,
600                                client_order_id,
601                                op.as_ref(),
602                                &error,
603                                &client,
604                                account_id,
605                                clock,
606                                &call_soon,
607                                &callback,
608                            );
609                        }
610                        OKXWsMessage::Error(msg) => {
611                            call_python_with_data(&call_soon, &callback, |py| msg.into_py_any(py));
612                        }
613                        OKXWsMessage::Reconnected => {
614                            quote_cache.clear();
615                        }
616                        OKXWsMessage::Authenticated => {}
617                    }
618                }
619            });
620
621            Ok(())
622        })
623    }
624
625    #[pyo3(name = "wait_until_active")]
626    fn py_wait_until_active<'py>(
627        &self,
628        py: Python<'py>,
629        timeout_secs: f64,
630    ) -> PyResult<Bound<'py, PyAny>> {
631        let client = self.clone();
632
633        pyo3_async_runtimes::tokio::future_into_py(py, async move {
634            client
635                .wait_until_active(timeout_secs)
636                .await
637                .map_err(to_pyruntime_err)?;
638            Ok(())
639        })
640    }
641
642    #[pyo3(name = "close")]
643    fn py_close<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
644        let mut client = self.clone();
645
646        pyo3_async_runtimes::tokio::future_into_py(py, async move {
647            if let Err(e) = client.close().await {
648                log::warn!("Error on close: {e}");
649            }
650            Ok(())
651        })
652    }
653
654    #[pyo3(name = "subscribe_instruments")]
655    fn py_subscribe_instruments<'py>(
656        &self,
657        py: Python<'py>,
658        instrument_type: OKXInstrumentType,
659    ) -> PyResult<Bound<'py, PyAny>> {
660        let client = self.clone();
661
662        pyo3_async_runtimes::tokio::future_into_py(py, async move {
663            if let Err(e) = client.subscribe_instruments(instrument_type).await {
664                log::error!("Failed to subscribe to instruments '{instrument_type}': {e}");
665            }
666            Ok(())
667        })
668    }
669
670    #[pyo3(name = "subscribe_instrument")]
671    fn py_subscribe_instrument<'py>(
672        &self,
673        py: Python<'py>,
674        instrument_id: InstrumentId,
675    ) -> PyResult<Bound<'py, PyAny>> {
676        let client = self.clone();
677
678        pyo3_async_runtimes::tokio::future_into_py(py, async move {
679            if let Err(e) = client.subscribe_instrument(instrument_id).await {
680                log::error!("Failed to subscribe to instrument {instrument_id}: {e}");
681            }
682            Ok(())
683        })
684    }
685
686    #[pyo3(name = "subscribe_book")]
687    fn py_subscribe_book<'py>(
688        &self,
689        py: Python<'py>,
690        instrument_id: InstrumentId,
691    ) -> PyResult<Bound<'py, PyAny>> {
692        let client = self.clone();
693
694        pyo3_async_runtimes::tokio::future_into_py(py, async move {
695            client
696                .subscribe_book(instrument_id)
697                .await
698                .map_err(to_pyvalue_err)
699        })
700    }
701
702    #[pyo3(name = "subscribe_book50_l2_tbt")]
703    fn py_subscribe_book50_l2_tbt<'py>(
704        &self,
705        py: Python<'py>,
706        instrument_id: InstrumentId,
707    ) -> PyResult<Bound<'py, PyAny>> {
708        let client = self.clone();
709
710        pyo3_async_runtimes::tokio::future_into_py(py, async move {
711            if let Err(e) = client.subscribe_book50_l2_tbt(instrument_id).await {
712                log::error!("Failed to subscribe to book50_tbt: {e}");
713            }
714            Ok(())
715        })
716    }
717
718    #[pyo3(name = "subscribe_book_l2_tbt")]
719    fn py_subscribe_book_l2_tbt<'py>(
720        &self,
721        py: Python<'py>,
722        instrument_id: InstrumentId,
723    ) -> PyResult<Bound<'py, PyAny>> {
724        let client = self.clone();
725
726        pyo3_async_runtimes::tokio::future_into_py(py, async move {
727            if let Err(e) = client.subscribe_book_l2_tbt(instrument_id).await {
728                log::error!("Failed to subscribe to books_l2_tbt: {e}");
729            }
730            Ok(())
731        })
732    }
733
734    #[pyo3(name = "subscribe_book_with_depth")]
735    fn py_subscribe_book_with_depth<'py>(
736        &self,
737        py: Python<'py>,
738        instrument_id: InstrumentId,
739        depth: u16,
740    ) -> PyResult<Bound<'py, PyAny>> {
741        let client = self.clone();
742
743        pyo3_async_runtimes::tokio::future_into_py(py, async move {
744            client
745                .subscribe_book_with_depth(instrument_id, depth)
746                .await
747                .map_err(to_pyvalue_err)
748        })
749    }
750
751    #[pyo3(name = "subscribe_book_depth5")]
752    fn py_subscribe_book_depth5<'py>(
753        &self,
754        py: Python<'py>,
755        instrument_id: InstrumentId,
756    ) -> PyResult<Bound<'py, PyAny>> {
757        let client = self.clone();
758
759        pyo3_async_runtimes::tokio::future_into_py(py, async move {
760            if let Err(e) = client.subscribe_book_depth5(instrument_id).await {
761                log::error!("Failed to subscribe to books5: {e}");
762            }
763            Ok(())
764        })
765    }
766
767    #[pyo3(name = "subscribe_quotes")]
768    fn py_subscribe_quotes<'py>(
769        &self,
770        py: Python<'py>,
771        instrument_id: InstrumentId,
772    ) -> PyResult<Bound<'py, PyAny>> {
773        let client = self.clone();
774
775        pyo3_async_runtimes::tokio::future_into_py(py, async move {
776            if let Err(e) = client.subscribe_quotes(instrument_id).await {
777                log::error!("Failed to subscribe to quotes: {e}");
778            }
779            Ok(())
780        })
781    }
782
783    #[pyo3(name = "subscribe_trades")]
784    fn py_subscribe_trades<'py>(
785        &self,
786        py: Python<'py>,
787        instrument_id: InstrumentId,
788        aggregated: bool,
789    ) -> PyResult<Bound<'py, PyAny>> {
790        let client = self.clone();
791
792        pyo3_async_runtimes::tokio::future_into_py(py, async move {
793            if let Err(e) = client.subscribe_trades(instrument_id, aggregated).await {
794                log::error!("Failed to subscribe to trades: {e}");
795            }
796            Ok(())
797        })
798    }
799
800    #[pyo3(name = "subscribe_bars")]
801    fn py_subscribe_bars<'py>(
802        &self,
803        py: Python<'py>,
804        bar_type: BarType,
805    ) -> PyResult<Bound<'py, PyAny>> {
806        let client = self.clone();
807
808        pyo3_async_runtimes::tokio::future_into_py(py, async move {
809            if let Err(e) = client.subscribe_bars(bar_type).await {
810                log::error!("Failed to subscribe to bars: {e}");
811            }
812            Ok(())
813        })
814    }
815
816    #[pyo3(name = "unsubscribe_book")]
817    fn py_unsubscribe_book<'py>(
818        &self,
819        py: Python<'py>,
820        instrument_id: InstrumentId,
821    ) -> PyResult<Bound<'py, PyAny>> {
822        let client = self.clone();
823
824        pyo3_async_runtimes::tokio::future_into_py(py, async move {
825            if let Err(e) = client.unsubscribe_book(instrument_id).await {
826                log::error!("Failed to unsubscribe from order book: {e}");
827            }
828            Ok(())
829        })
830    }
831
832    #[pyo3(name = "unsubscribe_book_depth5")]
833    fn py_unsubscribe_book_depth5<'py>(
834        &self,
835        py: Python<'py>,
836        instrument_id: InstrumentId,
837    ) -> PyResult<Bound<'py, PyAny>> {
838        let client = self.clone();
839
840        pyo3_async_runtimes::tokio::future_into_py(py, async move {
841            if let Err(e) = client.unsubscribe_book_depth5(instrument_id).await {
842                log::error!("Failed to unsubscribe from books5: {e}");
843            }
844            Ok(())
845        })
846    }
847
848    #[pyo3(name = "unsubscribe_book50_l2_tbt")]
849    fn py_unsubscribe_book50_l2_tbt<'py>(
850        &self,
851        py: Python<'py>,
852        instrument_id: InstrumentId,
853    ) -> PyResult<Bound<'py, PyAny>> {
854        let client = self.clone();
855
856        pyo3_async_runtimes::tokio::future_into_py(py, async move {
857            if let Err(e) = client.unsubscribe_book50_l2_tbt(instrument_id).await {
858                log::error!("Failed to unsubscribe from books50_l2_tbt: {e}");
859            }
860            Ok(())
861        })
862    }
863
864    #[pyo3(name = "unsubscribe_book_l2_tbt")]
865    fn py_unsubscribe_book_l2_tbt<'py>(
866        &self,
867        py: Python<'py>,
868        instrument_id: InstrumentId,
869    ) -> PyResult<Bound<'py, PyAny>> {
870        let client = self.clone();
871
872        pyo3_async_runtimes::tokio::future_into_py(py, async move {
873            if let Err(e) = client.unsubscribe_book_l2_tbt(instrument_id).await {
874                log::error!("Failed to unsubscribe from books_l2_tbt: {e}");
875            }
876            Ok(())
877        })
878    }
879
880    #[pyo3(name = "unsubscribe_quotes")]
881    fn py_unsubscribe_quotes<'py>(
882        &self,
883        py: Python<'py>,
884        instrument_id: InstrumentId,
885    ) -> PyResult<Bound<'py, PyAny>> {
886        let client = self.clone();
887
888        pyo3_async_runtimes::tokio::future_into_py(py, async move {
889            if let Err(e) = client.unsubscribe_quotes(instrument_id).await {
890                log::error!("Failed to unsubscribe from quotes: {e}");
891            }
892            Ok(())
893        })
894    }
895
896    #[pyo3(name = "unsubscribe_trades")]
897    fn py_unsubscribe_trades<'py>(
898        &self,
899        py: Python<'py>,
900        instrument_id: InstrumentId,
901        aggregated: bool,
902    ) -> PyResult<Bound<'py, PyAny>> {
903        let client = self.clone();
904
905        pyo3_async_runtimes::tokio::future_into_py(py, async move {
906            if let Err(e) = client.unsubscribe_trades(instrument_id, aggregated).await {
907                log::error!("Failed to unsubscribe from trades: {e}");
908            }
909            Ok(())
910        })
911    }
912
913    #[pyo3(name = "unsubscribe_bars")]
914    fn py_unsubscribe_bars<'py>(
915        &self,
916        py: Python<'py>,
917        bar_type: BarType,
918    ) -> PyResult<Bound<'py, PyAny>> {
919        let client = self.clone();
920
921        pyo3_async_runtimes::tokio::future_into_py(py, async move {
922            if let Err(e) = client.unsubscribe_bars(bar_type).await {
923                log::error!("Failed to unsubscribe from bars: {e}");
924            }
925            Ok(())
926        })
927    }
928
929    #[pyo3(name = "subscribe_ticker")]
930    fn py_subscribe_ticker<'py>(
931        &self,
932        py: Python<'py>,
933        instrument_id: InstrumentId,
934    ) -> PyResult<Bound<'py, PyAny>> {
935        let client = self.clone();
936
937        pyo3_async_runtimes::tokio::future_into_py(py, async move {
938            if let Err(e) = client.subscribe_ticker(instrument_id).await {
939                log::error!("Failed to subscribe to ticker: {e}");
940            }
941            Ok(())
942        })
943    }
944
945    #[pyo3(name = "unsubscribe_ticker")]
946    fn py_unsubscribe_ticker<'py>(
947        &self,
948        py: Python<'py>,
949        instrument_id: InstrumentId,
950    ) -> PyResult<Bound<'py, PyAny>> {
951        let client = self.clone();
952
953        pyo3_async_runtimes::tokio::future_into_py(py, async move {
954            if let Err(e) = client.unsubscribe_ticker(instrument_id).await {
955                log::error!("Failed to unsubscribe from ticker: {e}");
956            }
957            Ok(())
958        })
959    }
960
961    #[pyo3(name = "subscribe_mark_prices")]
962    fn py_subscribe_mark_prices<'py>(
963        &self,
964        py: Python<'py>,
965        instrument_id: InstrumentId,
966    ) -> PyResult<Bound<'py, PyAny>> {
967        let client = self.clone();
968
969        pyo3_async_runtimes::tokio::future_into_py(py, async move {
970            if let Err(e) = client.subscribe_mark_prices(instrument_id).await {
971                log::error!("Failed to subscribe to mark prices: {e}");
972            }
973            Ok(())
974        })
975    }
976
977    #[pyo3(name = "unsubscribe_mark_prices")]
978    fn py_unsubscribe_mark_prices<'py>(
979        &self,
980        py: Python<'py>,
981        instrument_id: InstrumentId,
982    ) -> PyResult<Bound<'py, PyAny>> {
983        let client = self.clone();
984
985        pyo3_async_runtimes::tokio::future_into_py(py, async move {
986            if let Err(e) = client.unsubscribe_mark_prices(instrument_id).await {
987                log::error!("Failed to unsubscribe from mark prices: {e}");
988            }
989            Ok(())
990        })
991    }
992
993    #[pyo3(name = "subscribe_index_prices")]
994    fn py_subscribe_index_prices<'py>(
995        &self,
996        py: Python<'py>,
997        instrument_id: InstrumentId,
998    ) -> PyResult<Bound<'py, PyAny>> {
999        let client = self.clone();
1000
1001        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1002            if let Err(e) = client.subscribe_index_prices(instrument_id).await {
1003                log::error!("Failed to subscribe to index prices: {e}");
1004            }
1005            Ok(())
1006        })
1007    }
1008
1009    #[pyo3(name = "unsubscribe_index_prices")]
1010    fn py_unsubscribe_index_prices<'py>(
1011        &self,
1012        py: Python<'py>,
1013        instrument_id: InstrumentId,
1014    ) -> PyResult<Bound<'py, PyAny>> {
1015        let client = self.clone();
1016
1017        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1018            if let Err(e) = client.unsubscribe_index_prices(instrument_id).await {
1019                log::error!("Failed to unsubscribe from index prices: {e}");
1020            }
1021            Ok(())
1022        })
1023    }
1024
1025    #[pyo3(name = "add_option_greeks_sub")]
1026    fn py_add_option_greeks_sub(&self, instrument_id: InstrumentId) {
1027        self.add_option_greeks_sub(instrument_id);
1028    }
1029
1030    #[pyo3(name = "add_option_greeks_sub_with_conventions")]
1031    fn py_add_option_greeks_sub_with_conventions(
1032        &self,
1033        instrument_id: InstrumentId,
1034        conventions: Vec<OKXGreeksType>,
1035    ) {
1036        self.add_option_greeks_sub_with_conventions(
1037            instrument_id,
1038            conventions.into_iter().collect(),
1039        );
1040    }
1041
1042    #[pyo3(name = "remove_option_greeks_sub")]
1043    fn py_remove_option_greeks_sub(&self, instrument_id: InstrumentId) {
1044        self.remove_option_greeks_sub(&instrument_id);
1045    }
1046
1047    #[pyo3(name = "subscribe_option_summary")]
1048    fn py_subscribe_option_summary<'py>(
1049        &self,
1050        py: Python<'py>,
1051        inst_family: &str,
1052    ) -> PyResult<Bound<'py, PyAny>> {
1053        let client = self.clone();
1054        let family = Ustr::from(inst_family);
1055
1056        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1057            if let Err(e) = client.subscribe_option_summary(family).await {
1058                log::error!("Failed to subscribe to option summary: {e}");
1059            }
1060            Ok(())
1061        })
1062    }
1063
1064    #[pyo3(name = "unsubscribe_option_summary")]
1065    fn py_unsubscribe_option_summary<'py>(
1066        &self,
1067        py: Python<'py>,
1068        inst_family: &str,
1069    ) -> PyResult<Bound<'py, PyAny>> {
1070        let client = self.clone();
1071        let family = Ustr::from(inst_family);
1072
1073        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1074            if let Err(e) = client.unsubscribe_option_summary(family).await {
1075                log::error!("Failed to unsubscribe from option summary: {e}");
1076            }
1077            Ok(())
1078        })
1079    }
1080
1081    #[pyo3(name = "subscribe_funding_rates")]
1082    fn py_subscribe_funding_rates<'py>(
1083        &self,
1084        py: Python<'py>,
1085        instrument_id: InstrumentId,
1086    ) -> PyResult<Bound<'py, PyAny>> {
1087        let client = self.clone();
1088
1089        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1090            if let Err(e) = client.subscribe_funding_rates(instrument_id).await {
1091                log::error!("Failed to subscribe to funding rates: {e}");
1092            }
1093            Ok(())
1094        })
1095    }
1096
1097    #[pyo3(name = "unsubscribe_funding_rates")]
1098    fn py_unsubscribe_funding_rates<'py>(
1099        &self,
1100        py: Python<'py>,
1101        instrument_id: InstrumentId,
1102    ) -> PyResult<Bound<'py, PyAny>> {
1103        let client = self.clone();
1104
1105        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1106            if let Err(e) = client.unsubscribe_funding_rates(instrument_id).await {
1107                log::error!("Failed to unsubscribe from funding rates: {e}");
1108            }
1109            Ok(())
1110        })
1111    }
1112
1113    #[pyo3(name = "subscribe_event_contract_markets")]
1114    fn py_subscribe_event_contract_markets<'py>(
1115        &self,
1116        py: Python<'py>,
1117    ) -> PyResult<Bound<'py, PyAny>> {
1118        let client = self.clone();
1119
1120        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1121            if let Err(e) = client.subscribe_event_contract_markets().await {
1122                log::error!("Failed to subscribe to event contract markets: {e}");
1123            }
1124            Ok(())
1125        })
1126    }
1127
1128    #[pyo3(name = "unsubscribe_event_contract_markets")]
1129    fn py_unsubscribe_event_contract_markets<'py>(
1130        &self,
1131        py: Python<'py>,
1132    ) -> PyResult<Bound<'py, PyAny>> {
1133        let client = self.clone();
1134
1135        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1136            if let Err(e) = client.unsubscribe_event_contract_markets().await {
1137                log::error!("Failed to unsubscribe from event contract markets: {e}");
1138            }
1139            Ok(())
1140        })
1141    }
1142
1143    #[pyo3(name = "subscribe_orders")]
1144    fn py_subscribe_orders<'py>(
1145        &self,
1146        py: Python<'py>,
1147        instrument_type: OKXInstrumentType,
1148    ) -> PyResult<Bound<'py, PyAny>> {
1149        let client = self.clone();
1150
1151        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1152            if let Err(e) = client.subscribe_orders(instrument_type).await {
1153                log::error!("Failed to subscribe to orders '{instrument_type}': {e}");
1154            }
1155            Ok(())
1156        })
1157    }
1158
1159    #[pyo3(name = "unsubscribe_orders")]
1160    fn py_unsubscribe_orders<'py>(
1161        &self,
1162        py: Python<'py>,
1163        instrument_type: OKXInstrumentType,
1164    ) -> PyResult<Bound<'py, PyAny>> {
1165        let client = self.clone();
1166
1167        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1168            if let Err(e) = client.unsubscribe_orders(instrument_type).await {
1169                log::error!("Failed to unsubscribe from orders '{instrument_type}': {e}");
1170            }
1171            Ok(())
1172        })
1173    }
1174
1175    #[pyo3(name = "subscribe_spread_orders")]
1176    fn py_subscribe_spread_orders<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
1177        let client = self.clone();
1178
1179        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1180            if let Err(e) = client.subscribe_spread_orders().await {
1181                log::error!("Failed to subscribe to spread orders: {e}");
1182            }
1183            Ok(())
1184        })
1185    }
1186
1187    #[pyo3(name = "unsubscribe_spread_orders")]
1188    fn py_unsubscribe_spread_orders<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
1189        let client = self.clone();
1190
1191        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1192            if let Err(e) = client.unsubscribe_spread_orders().await {
1193                log::error!("Failed to unsubscribe from spread orders: {e}");
1194            }
1195            Ok(())
1196        })
1197    }
1198
1199    #[pyo3(name = "subscribe_orders_algo")]
1200    fn py_subscribe_orders_algo<'py>(
1201        &self,
1202        py: Python<'py>,
1203        instrument_type: OKXInstrumentType,
1204    ) -> PyResult<Bound<'py, PyAny>> {
1205        let client = self.clone();
1206
1207        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1208            if let Err(e) = client.subscribe_orders_algo(instrument_type).await {
1209                log::error!("Failed to subscribe to algo orders '{instrument_type}': {e}");
1210            }
1211            Ok(())
1212        })
1213    }
1214
1215    #[pyo3(name = "unsubscribe_orders_algo")]
1216    fn py_unsubscribe_orders_algo<'py>(
1217        &self,
1218        py: Python<'py>,
1219        instrument_type: OKXInstrumentType,
1220    ) -> PyResult<Bound<'py, PyAny>> {
1221        let client = self.clone();
1222
1223        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1224            if let Err(e) = client.unsubscribe_orders_algo(instrument_type).await {
1225                log::error!("Failed to unsubscribe from algo orders '{instrument_type}': {e}");
1226            }
1227            Ok(())
1228        })
1229    }
1230
1231    #[pyo3(name = "subscribe_algo_advance")]
1232    fn py_subscribe_algo_advance<'py>(
1233        &self,
1234        py: Python<'py>,
1235        instrument_type: OKXInstrumentType,
1236    ) -> PyResult<Bound<'py, PyAny>> {
1237        let client = self.clone();
1238
1239        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1240            if let Err(e) = client.subscribe_algo_advance(instrument_type).await {
1241                log::error!("Failed to subscribe to algo-advance '{instrument_type}': {e}");
1242            }
1243            Ok(())
1244        })
1245    }
1246
1247    #[pyo3(name = "unsubscribe_algo_advance")]
1248    fn py_unsubscribe_algo_advance<'py>(
1249        &self,
1250        py: Python<'py>,
1251        instrument_type: OKXInstrumentType,
1252    ) -> PyResult<Bound<'py, PyAny>> {
1253        let client = self.clone();
1254
1255        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1256            if let Err(e) = client.unsubscribe_algo_advance(instrument_type).await {
1257                log::error!("Failed to unsubscribe from algo-advance '{instrument_type}': {e}");
1258            }
1259            Ok(())
1260        })
1261    }
1262
1263    #[pyo3(name = "subscribe_fills")]
1264    fn py_subscribe_fills<'py>(
1265        &self,
1266        py: Python<'py>,
1267        instrument_type: OKXInstrumentType,
1268    ) -> PyResult<Bound<'py, PyAny>> {
1269        let client = self.clone();
1270
1271        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1272            if let Err(e) = client.subscribe_fills(instrument_type).await {
1273                log::error!("Failed to subscribe to fills '{instrument_type}': {e}");
1274            }
1275            Ok(())
1276        })
1277    }
1278
1279    #[pyo3(name = "unsubscribe_fills")]
1280    fn py_unsubscribe_fills<'py>(
1281        &self,
1282        py: Python<'py>,
1283        instrument_type: OKXInstrumentType,
1284    ) -> PyResult<Bound<'py, PyAny>> {
1285        let client = self.clone();
1286
1287        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1288            if let Err(e) = client.unsubscribe_fills(instrument_type).await {
1289                log::error!("Failed to unsubscribe from fills '{instrument_type}': {e}");
1290            }
1291            Ok(())
1292        })
1293    }
1294
1295    #[pyo3(name = "subscribe_account")]
1296    fn py_subscribe_account<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
1297        let client = self.clone();
1298
1299        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1300            if let Err(e) = client.subscribe_account().await {
1301                log::error!("Failed to subscribe to account: {e}");
1302            }
1303            Ok(())
1304        })
1305    }
1306
1307    #[pyo3(name = "unsubscribe_account")]
1308    fn py_unsubscribe_account<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
1309        let client = self.clone();
1310
1311        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1312            if let Err(e) = client.unsubscribe_account().await {
1313                log::error!("Failed to unsubscribe from account: {e}");
1314            }
1315            Ok(())
1316        })
1317    }
1318
1319    #[pyo3(name = "submit_order")]
1320    #[pyo3(signature = (
1321        trader_id,
1322        strategy_id,
1323        instrument_id,
1324        td_mode,
1325        client_order_id,
1326        order_side,
1327        order_type,
1328        quantity,
1329        time_in_force=None,
1330        price=None,
1331        trigger_price=None,
1332        post_only=None,
1333        reduce_only=None,
1334        quote_quantity=None,
1335        position_side=None,
1336        attach_algo_ords=None,
1337        px_usd=None,
1338        px_vol=None,
1339        speed_bump=None,
1340        outcome=None,
1341        slippage_pct=None,
1342    ))]
1343    #[expect(clippy::too_many_arguments)]
1344    fn py_submit_order<'py>(
1345        &self,
1346        py: Python<'py>,
1347        trader_id: TraderId,
1348        strategy_id: StrategyId,
1349        instrument_id: InstrumentId,
1350        td_mode: OKXTradeMode,
1351        client_order_id: ClientOrderId,
1352        order_side: OrderSide,
1353        order_type: OrderType,
1354        quantity: Quantity,
1355        time_in_force: Option<TimeInForce>,
1356        price: Option<Price>,
1357        trigger_price: Option<Price>,
1358        post_only: Option<bool>,
1359        reduce_only: Option<bool>,
1360        quote_quantity: Option<bool>,
1361        position_side: Option<PositionSide>,
1362        attach_algo_ords: Option<Vec<Py<PyDict>>>,
1363        px_usd: Option<String>,
1364        px_vol: Option<String>,
1365        speed_bump: Option<String>,
1366        outcome: Option<String>,
1367        slippage_pct: Option<String>,
1368    ) -> PyResult<Bound<'py, PyAny>> {
1369        let attach_algo_ords = parse_attach_algo_ords(py, attach_algo_ords)?;
1370        let client = self.clone();
1371
1372        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1373            client
1374                .submit_order(
1375                    trader_id,
1376                    strategy_id,
1377                    instrument_id,
1378                    td_mode,
1379                    client_order_id,
1380                    order_side,
1381                    order_type,
1382                    quantity,
1383                    time_in_force,
1384                    price,
1385                    trigger_price,
1386                    post_only,
1387                    reduce_only,
1388                    quote_quantity,
1389                    position_side,
1390                    attach_algo_ords,
1391                    px_usd,
1392                    px_vol,
1393                    speed_bump,
1394                    outcome,
1395                    slippage_pct,
1396                )
1397                .await
1398                .map_err(to_pyvalue_err)
1399        })
1400    }
1401
1402    #[pyo3(name = "cancel_order", signature = (
1403        trader_id,
1404        strategy_id,
1405        instrument_id,
1406        client_order_id=None,
1407        venue_order_id=None,
1408    ))]
1409    fn py_cancel_order<'py>(
1410        &self,
1411        py: Python<'py>,
1412        trader_id: TraderId,
1413        strategy_id: StrategyId,
1414        instrument_id: InstrumentId,
1415        client_order_id: Option<ClientOrderId>,
1416        venue_order_id: Option<VenueOrderId>,
1417    ) -> PyResult<Bound<'py, PyAny>> {
1418        let client = self.clone();
1419
1420        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1421            client
1422                .cancel_order(
1423                    trader_id,
1424                    strategy_id,
1425                    instrument_id,
1426                    client_order_id,
1427                    venue_order_id,
1428                )
1429                .await
1430                .map_err(to_pyvalue_err)
1431        })
1432    }
1433
1434    #[pyo3(name = "modify_order")]
1435    #[pyo3(signature = (
1436        trader_id,
1437        strategy_id,
1438        instrument_id,
1439        client_order_id=None,
1440        venue_order_id=None,
1441        price=None,
1442        quantity=None,
1443        new_px_usd=None,
1444        new_px_vol=None,
1445        speed_bump=None,
1446    ))]
1447    #[expect(clippy::too_many_arguments)]
1448    fn py_modify_order<'py>(
1449        &self,
1450        py: Python<'py>,
1451        trader_id: TraderId,
1452        strategy_id: StrategyId,
1453        instrument_id: InstrumentId,
1454        client_order_id: Option<ClientOrderId>,
1455        venue_order_id: Option<VenueOrderId>,
1456        price: Option<Price>,
1457        quantity: Option<Quantity>,
1458        new_px_usd: Option<String>,
1459        new_px_vol: Option<String>,
1460        speed_bump: Option<String>,
1461    ) -> PyResult<Bound<'py, PyAny>> {
1462        let client = self.clone();
1463
1464        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1465            client
1466                .modify_order(
1467                    trader_id,
1468                    strategy_id,
1469                    instrument_id,
1470                    client_order_id,
1471                    price,
1472                    quantity,
1473                    venue_order_id,
1474                    new_px_usd,
1475                    new_px_vol,
1476                    speed_bump,
1477                )
1478                .await
1479                .map_err(to_pyvalue_err)
1480        })
1481    }
1482
1483    #[pyo3(name = "batch_submit_orders")]
1484    fn py_batch_submit_orders<'py>(
1485        &self,
1486        py: Python<'py>,
1487        orders: Vec<Py<PyAny>>,
1488    ) -> PyResult<Bound<'py, PyAny>> {
1489        let mut domain_orders = Vec::with_capacity(orders.len());
1490
1491        for obj in orders {
1492            let (
1493                instrument_type,
1494                instrument_id,
1495                td_mode,
1496                client_order_id,
1497                order_side,
1498                order_type,
1499                quantity,
1500                position_side,
1501                price,
1502                trigger_price,
1503                post_only,
1504                reduce_only,
1505                speed_bump,
1506                outcome,
1507            ) = extract_batch_submit_order(py, &obj)?;
1508
1509            domain_orders.push((
1510                instrument_type,
1511                instrument_id,
1512                td_mode,
1513                client_order_id,
1514                order_side,
1515                position_side,
1516                order_type,
1517                quantity,
1518                price,
1519                trigger_price,
1520                post_only,
1521                reduce_only,
1522                speed_bump,
1523                outcome,
1524            ));
1525        }
1526
1527        let client = self.clone();
1528
1529        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1530            client
1531                .batch_submit_orders(domain_orders)
1532                .await
1533                .map_err(to_pyvalue_err)
1534        })
1535    }
1536
1537    /// Cancels multiple orders via WebSocket.
1538    #[pyo3(name = "batch_cancel_orders")]
1539    fn py_batch_cancel_orders<'py>(
1540        &self,
1541        py: Python<'py>,
1542        cancels: Vec<Py<PyAny>>,
1543    ) -> PyResult<Bound<'py, PyAny>> {
1544        let mut batched_cancels = Vec::with_capacity(cancels.len());
1545
1546        for obj in cancels {
1547            let (instrument_id, client_order_id, order_id): (
1548                InstrumentId,
1549                Option<ClientOrderId>,
1550                Option<VenueOrderId>,
1551            ) = obj.extract(py).map_err(to_pyruntime_err)?;
1552            batched_cancels.push((instrument_id, client_order_id, order_id));
1553        }
1554
1555        let client = self.clone();
1556
1557        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1558            client
1559                .batch_cancel_orders(batched_cancels)
1560                .await
1561                .map_err(to_pyvalue_err)
1562        })
1563    }
1564
1565    #[pyo3(name = "batch_modify_orders")]
1566    fn py_batch_modify_orders<'py>(
1567        &self,
1568        py: Python<'py>,
1569        orders: Vec<Py<PyAny>>,
1570    ) -> PyResult<Bound<'py, PyAny>> {
1571        let mut domain_orders = Vec::with_capacity(orders.len());
1572
1573        for obj in orders {
1574            let extracted_with_event_params: PyResult<PyBatchModifyOrder> =
1575                obj.extract(py).map_err(to_pyruntime_err);
1576
1577            let (
1578                instrument_type,
1579                instrument_id,
1580                client_order_id,
1581                new_client_order_id,
1582                price,
1583                quantity,
1584                speed_bump,
1585            ) = if let Ok(values) = extracted_with_event_params {
1586                values
1587            } else {
1588                let (
1589                    instrument_type,
1590                    instrument_id,
1591                    client_order_id,
1592                    new_client_order_id,
1593                    price,
1594                    quantity,
1595                ): PyLegacyBatchModifyOrder = obj.extract(py).map_err(to_pyruntime_err)?;
1596
1597                (
1598                    instrument_type,
1599                    instrument_id,
1600                    client_order_id,
1601                    new_client_order_id,
1602                    price,
1603                    quantity,
1604                    None,
1605                )
1606            };
1607            let inst_type =
1608                OKXInstrumentType::from_str(&instrument_type).map_err(to_pyvalue_err)?;
1609            domain_orders.push((
1610                inst_type,
1611                instrument_id,
1612                client_order_id,
1613                new_client_order_id,
1614                price,
1615                quantity,
1616                speed_bump,
1617            ));
1618        }
1619
1620        let client = self.clone();
1621
1622        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1623            client
1624                .batch_modify_orders(domain_orders)
1625                .await
1626                .map_err(to_pyvalue_err)
1627        })
1628    }
1629
1630    #[pyo3(name = "mass_cancel_orders")]
1631    fn py_mass_cancel_orders<'py>(
1632        &self,
1633        py: Python<'py>,
1634        instrument_id: InstrumentId,
1635    ) -> PyResult<Bound<'py, PyAny>> {
1636        let client = self.clone();
1637
1638        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1639            client
1640                .mass_cancel_orders(instrument_id)
1641                .await
1642                .map_err(to_pyvalue_err)
1643        })
1644    }
1645
1646    #[pyo3(name = "cache_instruments")]
1647    fn py_cache_instruments(&self, py: Python<'_>, instruments: Vec<Py<PyAny>>) -> PyResult<()> {
1648        let instruments: Result<Vec<_>, _> = instruments
1649            .into_iter()
1650            .map(|inst| pyobject_to_instrument_any(py, inst))
1651            .collect();
1652        self.cache_instruments(&instruments?);
1653        Ok(())
1654    }
1655
1656    #[pyo3(name = "cache_instrument")]
1657    fn py_cache_instrument(&self, py: Python<'_>, instrument: Py<PyAny>) -> PyResult<()> {
1658        self.cache_instrument(pyobject_to_instrument_any(py, instrument)?);
1659        Ok(())
1660    }
1661
1662    #[pyo3(name = "cache_inst_id_codes")]
1663    fn py_cache_inst_id_codes(&self, mappings: Vec<(String, u64)>) {
1664        let ustr_mappings = mappings
1665            .into_iter()
1666            .map(|(inst_id, code)| (Ustr::from(&inst_id), code));
1667        self.cache_inst_id_codes(ustr_mappings);
1668    }
1669}
1670
1671fn handle_book_data(
1672    inst_id: Option<Ustr>,
1673    action: OKXBookAction,
1674    data: Vec<OKXBookMsg>,
1675    instruments_by_symbol: &AHashMap<Ustr, InstrumentAny>,
1676    clock: &AtomicTime,
1677    call_soon: &Py<PyAny>,
1678    callback: &Py<PyAny>,
1679) {
1680    let Some(inst_id) = inst_id else { return };
1681    let Some(instrument) = instruments_by_symbol.get(&inst_id) else {
1682        log::warn!("No cached instrument for book data: {inst_id}");
1683        return;
1684    };
1685    let ts_init = clock.get_time_ns();
1686
1687    match parse_book_msg_vec(
1688        data,
1689        &instrument.id(),
1690        instrument.price_precision(),
1691        instrument.size_precision(),
1692        action,
1693        ts_init,
1694    ) {
1695        Ok(data_vec) => Python::attach(|py| {
1696            for d in data_vec {
1697                let py_obj = data_to_pycapsule(py, d);
1698                call_python_threadsafe(py, call_soon, callback, py_obj);
1699            }
1700        }),
1701        Err(e) => log::error!("Failed to parse book data: {e}"),
1702    }
1703}
1704
1705#[expect(clippy::too_many_arguments)]
1706fn handle_channel_data(
1707    channel: &OKXWsChannel,
1708    inst_id: Option<Ustr>,
1709    data: serde_json::Value,
1710    instruments_by_symbol: &mut AHashMap<Ustr, InstrumentAny>,
1711    quote_cache: &mut QuoteCache,
1712    funding_cache: &mut AHashMap<Ustr, (Ustr, u64)>,
1713    option_greeks_subs: &AHashMap<InstrumentId, AHashSet<OKXGreeksType>>,
1714    clock: &AtomicTime,
1715    call_soon: &Py<PyAny>,
1716    callback: &Py<PyAny>,
1717) {
1718    if matches!(channel, OKXWsChannel::OptionSummary) {
1719        let ts_init = clock.get_time_ns();
1720
1721        match serde_json::from_value::<Vec<OKXOptionSummaryMsg>>(data) {
1722            Ok(msgs) => {
1723                for msg in &msgs {
1724                    let Some(instrument) = instruments_by_symbol.get(&msg.inst_id) else {
1725                        continue;
1726                    };
1727                    let instrument_id = instrument.id();
1728                    let Some(conventions) = option_greeks_subs.get(&instrument_id) else {
1729                        continue;
1730                    };
1731
1732                    for greeks_type in conventions {
1733                        match parse_option_summary_greeks(
1734                            msg,
1735                            &instrument_id,
1736                            *greeks_type,
1737                            ts_init,
1738                        ) {
1739                            Ok(greeks) => {
1740                                Python::attach(|py| match greeks.into_py_any(py) {
1741                                    Ok(py_obj) => {
1742                                        call_python_threadsafe(py, call_soon, callback, py_obj);
1743                                    }
1744                                    Err(e) => {
1745                                        log::error!(
1746                                            "Failed to convert OptionGreeks to Python: {e}"
1747                                        );
1748                                    }
1749                                });
1750                            }
1751                            Err(e) => {
1752                                log::error!(
1753                                    "Failed to parse option summary for {} ({greeks_type:?}): {e}",
1754                                    msg.inst_id
1755                                );
1756                            }
1757                        }
1758                    }
1759                }
1760            }
1761            Err(e) => log::error!("Failed to deserialize option summary data: {e}"),
1762        }
1763        return;
1764    }
1765
1766    if matches!(channel, OKXWsChannel::EventContractMarkets) {
1767        dispatch_json_value_to_python(&data, call_soon, callback);
1768        return;
1769    }
1770
1771    let Some(inst_id) = inst_id else { return };
1772
1773    if matches!(channel, OKXWsChannel::IndexTickers) {
1774        let ts_init = clock.get_time_ns();
1775        let prefix = format!("{inst_id}-");
1776        let matching: Vec<_> = instruments_by_symbol
1777            .values()
1778            .filter(|i| {
1779                let s = i.symbol().inner();
1780                s == inst_id || s.as_str().starts_with(&prefix)
1781            })
1782            .collect();
1783
1784        for instrument in matching {
1785            if let Ok(data_vec) = parse_index_price_msg_vec(
1786                data.clone(),
1787                &instrument.id(),
1788                instrument.price_precision(),
1789                ts_init,
1790            ) {
1791                Python::attach(|py| {
1792                    for d in data_vec {
1793                        let py_obj = data_to_pycapsule(py, d);
1794                        call_python_threadsafe(py, call_soon, callback, py_obj);
1795                    }
1796                });
1797            }
1798        }
1799        return;
1800    }
1801
1802    let Some(instrument) = instruments_by_symbol.get(&inst_id) else {
1803        log::warn!("No cached instrument for {channel:?}: {inst_id}");
1804        return;
1805    };
1806    let instrument_id = instrument.id();
1807    let price_precision = instrument.price_precision();
1808    let size_precision = instrument.size_precision();
1809    let ts_init = clock.get_time_ns();
1810
1811    if matches!(channel, OKXWsChannel::BboTbt) {
1812        handle_bbo_tbt(
1813            data,
1814            instrument_id,
1815            price_precision,
1816            size_precision,
1817            ts_init,
1818            quote_cache,
1819            call_soon,
1820            callback,
1821        );
1822        return;
1823    }
1824
1825    match parse_ws_message_data(
1826        channel,
1827        data,
1828        &instrument_id,
1829        price_precision,
1830        size_precision,
1831        ts_init,
1832        funding_cache,
1833        instruments_by_symbol,
1834    ) {
1835        Ok(Some(ws_msg)) => {
1836            dispatch_nautilus_ws_msg_to_python(ws_msg, call_soon, callback, instruments_by_symbol);
1837        }
1838        Ok(None) => {}
1839        Err(e) => {
1840            log::error!("Failed to parse {channel:?} data: {e}");
1841        }
1842    }
1843}
1844
1845#[expect(clippy::too_many_arguments)]
1846fn handle_bbo_tbt(
1847    data: serde_json::Value,
1848    instrument_id: InstrumentId,
1849    price_precision: u8,
1850    size_precision: u8,
1851    ts_init: UnixNanos,
1852    quote_cache: &mut QuoteCache,
1853    call_soon: &Py<PyAny>,
1854    callback: &Py<PyAny>,
1855) {
1856    let msgs: Vec<OKXBookMsg> = match serde_json::from_value(data) {
1857        Ok(msgs) => msgs,
1858        Err(e) => {
1859            log::error!("Failed to deserialize BboTbt data: {e}");
1860            return;
1861        }
1862    };
1863
1864    for msg in &msgs {
1865        let bid = msg.bids.first();
1866        let ask = msg.asks.first();
1867
1868        let bid_price = bid.and_then(|e| parse_price(&e.price, price_precision).ok());
1869        let bid_size = bid.and_then(|e| parse_quantity(&e.size, size_precision).ok());
1870        let ask_price = ask.and_then(|e| parse_price(&e.price, price_precision).ok());
1871        let ask_size = ask.and_then(|e| parse_quantity(&e.size, size_precision).ok());
1872        let ts_event = parse_millisecond_timestamp(msg.ts);
1873
1874        match quote_cache.process(
1875            instrument_id,
1876            bid_price,
1877            ask_price,
1878            bid_size,
1879            ask_size,
1880            ts_event,
1881            ts_init,
1882        ) {
1883            Ok(quote) => {
1884                Python::attach(|py| {
1885                    let py_obj = data_to_pycapsule(py, Data::Quote(quote));
1886                    call_python_threadsafe(py, call_soon, callback, py_obj);
1887                });
1888            }
1889            Err(e) => {
1890                log::debug!("Skipping partial BboTbt for {instrument_id}: {e}");
1891            }
1892        }
1893    }
1894}
1895
1896fn handle_instruments(
1897    okx_instruments: Vec<OKXInstrument>,
1898    instruments_by_symbol: &mut AHashMap<Ustr, InstrumentAny>,
1899    clock: &AtomicTime,
1900    call_soon: &Py<PyAny>,
1901    callback: &Py<PyAny>,
1902) {
1903    let ts_init = clock.get_time_ns();
1904
1905    for okx_inst in okx_instruments {
1906        let inst_key = Ustr::from(&okx_inst.inst_id);
1907        let (margin_init, margin_maint, maker_fee, taker_fee) =
1908            instruments_by_symbol.get(&inst_key).map_or(
1909                (None, None, None, None),
1910                extract_fees_from_cached_instrument,
1911            );
1912        let status_action = okx_status_to_market_action(okx_inst.state);
1913        let is_live = matches!(okx_inst.state, OKXInstrumentStatus::Live);
1914
1915        match parse_instrument_any(
1916            &okx_inst,
1917            margin_init,
1918            margin_maint,
1919            maker_fee,
1920            taker_fee,
1921            ts_init,
1922        ) {
1923            Ok(Some(inst_any)) => {
1924                let instrument_id = inst_any.id();
1925                instruments_by_symbol.insert(inst_any.symbol().inner(), inst_any.clone());
1926                call_python_with_data(call_soon, callback, |py| {
1927                    instrument_any_to_pyobject(py, inst_any)
1928                });
1929                let status = InstrumentStatus::new(
1930                    instrument_id,
1931                    status_action,
1932                    ts_init,
1933                    ts_init,
1934                    None,
1935                    None,
1936                    Some(is_live),
1937                    None,
1938                    None,
1939                );
1940                call_python_with_data(call_soon, callback, |py| status.into_py_any(py));
1941            }
1942            Ok(None) => {
1943                let instrument_id = instruments_by_symbol
1944                    .get(&inst_key)
1945                    .map_or_else(|| parse_instrument_id(inst_key), |i| i.id());
1946                let status = InstrumentStatus::new(
1947                    instrument_id,
1948                    status_action,
1949                    ts_init,
1950                    ts_init,
1951                    None,
1952                    None,
1953                    Some(is_live),
1954                    None,
1955                    None,
1956                );
1957                call_python_with_data(call_soon, callback, |py| status.into_py_any(py));
1958            }
1959            Err(e) => {
1960                log::warn!("Failed to parse instrument {}: {e}", okx_inst.inst_id);
1961                let instrument_id = instruments_by_symbol
1962                    .get(&inst_key)
1963                    .map_or_else(|| parse_instrument_id(inst_key), |i| i.id());
1964                let status = InstrumentStatus::new(
1965                    instrument_id,
1966                    status_action,
1967                    ts_init,
1968                    ts_init,
1969                    None,
1970                    None,
1971                    Some(is_live),
1972                    None,
1973                    None,
1974                );
1975                call_python_with_data(call_soon, callback, |py| status.into_py_any(py));
1976            }
1977        }
1978    }
1979}
1980
1981#[expect(clippy::too_many_arguments)]
1982fn handle_orders(
1983    order_msgs: &[OKXOrderMsg],
1984    account_id: AccountId,
1985    instruments_by_symbol: &AHashMap<Ustr, InstrumentAny>,
1986    fee_cache: &mut AHashMap<Ustr, Money>,
1987    filled_qty_cache: &mut AHashMap<Ustr, Quantity>,
1988    clock: &AtomicTime,
1989    call_soon: &Py<PyAny>,
1990    callback: &Py<PyAny>,
1991) {
1992    let ts_init = clock.get_time_ns();
1993
1994    match parse_order_msg_vec(
1995        order_msgs,
1996        account_id,
1997        instruments_by_symbol,
1998        fee_cache,
1999        filled_qty_cache,
2000        ts_init,
2001    ) {
2002        Ok(reports) => {
2003            dispatch_execution_reports_to_python(reports, call_soon, callback);
2004        }
2005        Err(e) => {
2006            log::error!("Failed to parse order messages: {e}");
2007        }
2008    }
2009}
2010
2011fn handle_spread_orders(
2012    order_msgs: &[OKXSpreadOrder],
2013    account_id: AccountId,
2014    instruments_by_symbol: &AHashMap<Ustr, InstrumentAny>,
2015    filled_qty_cache: &mut AHashMap<Ustr, Quantity>,
2016    clock: &AtomicTime,
2017    call_soon: &Py<PyAny>,
2018    callback: &Py<PyAny>,
2019) {
2020    let ts_init = clock.get_time_ns();
2021    let mut reports = Vec::with_capacity(order_msgs.len());
2022
2023    for msg in order_msgs {
2024        match parse_spread_order_msg(
2025            msg,
2026            account_id,
2027            instruments_by_symbol,
2028            filled_qty_cache,
2029            ts_init,
2030        ) {
2031            Ok(report) => {
2032                if let Some(instrument) = instruments_by_symbol.get(&msg.sprd_id)
2033                    && !msg.acc_fill_sz.is_empty()
2034                    && msg.acc_fill_sz != "0"
2035                    && let Ok(qty) = parse_quantity(&msg.acc_fill_sz, instrument.size_precision())
2036                {
2037                    filled_qty_cache.insert(msg.ord_id, qty);
2038                }
2039                reports.push(report);
2040            }
2041            Err(e) => log::error!("Failed to parse spread order message: {e}"),
2042        }
2043    }
2044
2045    dispatch_execution_reports_to_python(reports, call_soon, callback);
2046}
2047
2048fn handle_algo_orders(
2049    algo_msgs: Vec<OKXAlgoOrderMsg>,
2050    account_id: AccountId,
2051    instruments_by_symbol: &AHashMap<Ustr, InstrumentAny>,
2052    clock: &AtomicTime,
2053    call_soon: &Py<PyAny>,
2054    callback: &Py<PyAny>,
2055) {
2056    let ts_init = clock.get_time_ns();
2057    for algo_msg in algo_msgs {
2058        match parse_algo_order_msg(&algo_msg, account_id, instruments_by_symbol, ts_init) {
2059            Ok(Some(report)) => {
2060                dispatch_execution_reports_to_python(vec![report], call_soon, callback);
2061            }
2062            Ok(None) => {}
2063            Err(e) => {
2064                log::error!("Failed to parse algo order: {e}");
2065            }
2066        }
2067    }
2068}
2069
2070fn handle_account(
2071    data: serde_json::Value,
2072    account_id: AccountId,
2073    clock: &AtomicTime,
2074    call_soon: &Py<PyAny>,
2075    callback: &Py<PyAny>,
2076) {
2077    if let Ok(accounts) = serde_json::from_value::<Vec<OKXAccount>>(data) {
2078        let ts_init = clock.get_time_ns();
2079        for account in &accounts {
2080            if let Ok(account_state) = parse_account_state(account, account_id, ts_init) {
2081                call_python_with_data(call_soon, callback, |py| account_state.into_py_any(py));
2082            }
2083        }
2084    }
2085}
2086
2087fn handle_positions(
2088    data: serde_json::Value,
2089    account_id: AccountId,
2090    instruments_by_symbol: &AHashMap<Ustr, InstrumentAny>,
2091    clock: &AtomicTime,
2092    call_soon: &Py<PyAny>,
2093    callback: &Py<PyAny>,
2094) {
2095    if let Ok(positions) = serde_json::from_value::<Vec<OKXPosition>>(data) {
2096        let ts_init = clock.get_time_ns();
2097
2098        for position in positions {
2099            let inst_key = Ustr::from(&position.inst_id);
2100            if let Some(instrument) = instruments_by_symbol.get(&inst_key) {
2101                match parse_position_status_report(
2102                    &position,
2103                    account_id,
2104                    instrument.id(),
2105                    instrument.size_precision(),
2106                    ts_init,
2107                ) {
2108                    Ok(report) => {
2109                        call_python_with_data(call_soon, callback, |py| report.into_py_any(py));
2110                    }
2111                    Err(e) => {
2112                        log::error!("Failed to parse position: {e}");
2113                    }
2114                }
2115            }
2116        }
2117    }
2118}
2119
2120#[expect(clippy::too_many_arguments)]
2121fn handle_order_response(
2122    id: Option<&str>,
2123    op: &OKXWsOperation,
2124    code: &str,
2125    msg: &str,
2126    data: &[serde_json::Value],
2127    client: &OKXWebSocketClient,
2128    account_id: AccountId,
2129    clock: &AtomicTime,
2130    call_soon: &Py<PyAny>,
2131    callback: &Py<PyAny>,
2132) {
2133    for item in data {
2134        let s_code = item
2135            .get(OKX_FIELD_SCODE)
2136            .and_then(|v| v.as_str())
2137            .unwrap_or("");
2138        let s_msg = item
2139            .get(OKX_FIELD_SMSG)
2140            .and_then(|v| v.as_str())
2141            .unwrap_or("");
2142        let cl_ord_id = item
2143            .get(OKX_FIELD_CLORDID)
2144            .and_then(|v| v.as_str())
2145            .unwrap_or("");
2146
2147        if s_code == OKX_SUCCESS_CODE {
2148            log::debug!("Order response ok: op={op:?} cl_ord_id={cl_ord_id}");
2149            match op {
2150                OKXWsOperation::Order | OKXWsOperation::BatchOrders => {
2151                    if let Some((_, info)) = client.pending_orders.remove(cl_ord_id) {
2152                        let venue_order_id = item
2153                            .get("ordId")
2154                            .and_then(|v| v.as_str())
2155                            .filter(|s| !s.is_empty());
2156
2157                        if let Some(ord_id) = venue_order_id {
2158                            let ts_init = clock.get_time_ns();
2159                            let accepted = OrderAccepted::new(
2160                                info.trader_id,
2161                                info.strategy_id,
2162                                info.instrument_id,
2163                                ClientOrderId::from(cl_ord_id),
2164                                VenueOrderId::new(ord_id),
2165                                account_id,
2166                                UUID4::new(),
2167                                ts_init,
2168                                ts_init,
2169                                false,
2170                            );
2171                            call_python_with_data(call_soon, callback, |py| {
2172                                accepted.into_py_any(py)
2173                            });
2174                        } else {
2175                            log::error!(
2176                                "No venue_order_id for accepted order: cl_ord_id={cl_ord_id}"
2177                            );
2178                        }
2179                    }
2180                }
2181                OKXWsOperation::OrderAlgo => {
2182                    client.pending_orders.remove(cl_ord_id);
2183                    log::debug!("Algo order placement confirmed: cl_ord_id={cl_ord_id}");
2184                }
2185                OKXWsOperation::CancelOrder
2186                | OKXWsOperation::BatchCancelOrders
2187                | OKXWsOperation::MassCancel
2188                | OKXWsOperation::CancelAlgos => {
2189                    client.pending_cancels.remove(cl_ord_id);
2190                }
2191                OKXWsOperation::AmendOrder | OKXWsOperation::BatchAmendOrders => {
2192                    client.pending_amends.remove(cl_ord_id);
2193                }
2194                _ => {}
2195            }
2196        } else if !cl_ord_id.is_empty() {
2197            log::warn!(
2198                "Order response rejected: op={op:?} cl_ord_id={cl_ord_id} \
2199                 s_code={s_code} s_msg={s_msg}"
2200            );
2201            let ts_init = clock.get_time_ns();
2202            let client_order_id = ClientOrderId::from(cl_ord_id);
2203            let venue_order_id = item
2204                .get("ordId")
2205                .and_then(|v| v.as_str())
2206                .filter(|s| !s.is_empty())
2207                .map(VenueOrderId::new);
2208
2209            match op {
2210                OKXWsOperation::Order | OKXWsOperation::BatchOrders | OKXWsOperation::OrderAlgo => {
2211                    if let Some((_, info)) = client.pending_orders.remove(cl_ord_id) {
2212                        let rejected = OrderRejected::new(
2213                            info.trader_id,
2214                            info.strategy_id,
2215                            info.instrument_id,
2216                            client_order_id,
2217                            account_id,
2218                            Ustr::from(s_msg),
2219                            UUID4::new(),
2220                            ts_init,
2221                            ts_init,
2222                            false,
2223                            false,
2224                        );
2225                        call_python_with_data(call_soon, callback, |py| rejected.into_py_any(py));
2226                    }
2227                }
2228                OKXWsOperation::CancelOrder
2229                | OKXWsOperation::BatchCancelOrders
2230                | OKXWsOperation::MassCancel
2231                | OKXWsOperation::CancelAlgos => {
2232                    if let Some((_, info)) = client.pending_cancels.remove(cl_ord_id) {
2233                        let rejected = OrderCancelRejected::new(
2234                            info.trader_id,
2235                            info.strategy_id,
2236                            info.instrument_id,
2237                            client_order_id,
2238                            Ustr::from(s_msg),
2239                            UUID4::new(),
2240                            ts_init,
2241                            ts_init,
2242                            false,
2243                            venue_order_id,
2244                            Some(account_id),
2245                        );
2246                        call_python_with_data(call_soon, callback, |py| rejected.into_py_any(py));
2247                    }
2248                }
2249                OKXWsOperation::AmendOrder | OKXWsOperation::BatchAmendOrders => {
2250                    if let Some((_, info)) = client.pending_amends.remove(cl_ord_id) {
2251                        let rejected = OrderModifyRejected::new(
2252                            info.trader_id,
2253                            info.strategy_id,
2254                            info.instrument_id,
2255                            client_order_id,
2256                            Ustr::from(s_msg),
2257                            UUID4::new(),
2258                            ts_init,
2259                            ts_init,
2260                            false,
2261                            venue_order_id,
2262                            Some(account_id),
2263                        );
2264                        call_python_with_data(call_soon, callback, |py| rejected.into_py_any(py));
2265                    }
2266                }
2267                _ => {}
2268            }
2269        }
2270    }
2271
2272    if code != "0" && data.is_empty() {
2273        log::warn!("Order response error (no data): id={id:?} op={op:?} code={code} msg={msg}");
2274    }
2275}
2276
2277#[expect(clippy::too_many_arguments)]
2278fn handle_send_failed(
2279    request_id: &str,
2280    client_order_id: Option<ClientOrderId>,
2281    op: Option<&OKXWsOperation>,
2282    error: &str,
2283    client: &OKXWebSocketClient,
2284    account_id: AccountId,
2285    clock: &AtomicTime,
2286    call_soon: &Py<PyAny>,
2287    callback: &Py<PyAny>,
2288) {
2289    log::error!("WebSocket send failed: request_id={request_id} error={error}");
2290
2291    let Some(client_order_id) = client_order_id else {
2292        return;
2293    };
2294    let cl_ord_str = client_order_id.to_string();
2295    let ts_init = clock.get_time_ns();
2296
2297    match op {
2298        Some(OKXWsOperation::Order | OKXWsOperation::BatchOrders | OKXWsOperation::OrderAlgo) => {
2299            if let Some((_, info)) = client.pending_orders.remove(&cl_ord_str) {
2300                let rejected = OrderRejected::new(
2301                    info.trader_id,
2302                    info.strategy_id,
2303                    info.instrument_id,
2304                    client_order_id,
2305                    account_id,
2306                    Ustr::from(error),
2307                    UUID4::new(),
2308                    ts_init,
2309                    ts_init,
2310                    false,
2311                    false,
2312                );
2313                call_python_with_data(call_soon, callback, |py| rejected.into_py_any(py));
2314            }
2315        }
2316        Some(
2317            OKXWsOperation::CancelOrder
2318            | OKXWsOperation::BatchCancelOrders
2319            | OKXWsOperation::MassCancel
2320            | OKXWsOperation::CancelAlgos,
2321        ) => {
2322            if let Some((_, info)) = client.pending_cancels.remove(&cl_ord_str) {
2323                let rejected = OrderCancelRejected::new(
2324                    info.trader_id,
2325                    info.strategy_id,
2326                    info.instrument_id,
2327                    client_order_id,
2328                    Ustr::from(error),
2329                    UUID4::new(),
2330                    ts_init,
2331                    ts_init,
2332                    false,
2333                    None,
2334                    Some(account_id),
2335                );
2336                call_python_with_data(call_soon, callback, |py| rejected.into_py_any(py));
2337            }
2338        }
2339        Some(OKXWsOperation::AmendOrder | OKXWsOperation::BatchAmendOrders) => {
2340            if let Some((_, info)) = client.pending_amends.remove(&cl_ord_str) {
2341                let rejected = OrderModifyRejected::new(
2342                    info.trader_id,
2343                    info.strategy_id,
2344                    info.instrument_id,
2345                    client_order_id,
2346                    Ustr::from(error),
2347                    UUID4::new(),
2348                    ts_init,
2349                    ts_init,
2350                    false,
2351                    None,
2352                    Some(account_id),
2353                );
2354                call_python_with_data(call_soon, callback, |py| rejected.into_py_any(py));
2355            }
2356        }
2357        _ => {
2358            log::warn!("SendFailed for {client_order_id} with unknown op, cannot emit rejection");
2359        }
2360    }
2361}
2362
2363fn call_python_with_data<F>(call_soon: &Py<PyAny>, callback: &Py<PyAny>, data_converter: F)
2364where
2365    F: FnOnce(Python) -> PyResult<Py<PyAny>>,
2366{
2367    Python::attach(|py| match data_converter(py) {
2368        Ok(py_obj) => call_python_threadsafe(py, call_soon, callback, py_obj),
2369        Err(e) => log::error!("Failed to convert data to Python object: {e}"),
2370    });
2371}
2372
2373fn dispatch_json_value_to_python(
2374    data: &serde_json::Value,
2375    call_soon: &Py<PyAny>,
2376    callback: &Py<PyAny>,
2377) {
2378    call_python_with_data(call_soon, callback, |py| value_to_pyobject(py, data));
2379}
2380
2381fn dispatch_nautilus_ws_msg_to_python(
2382    msg: NautilusWsMessage,
2383    call_soon: &Py<PyAny>,
2384    callback: &Py<PyAny>,
2385    instruments_by_symbol: &mut AHashMap<Ustr, InstrumentAny>,
2386) {
2387    match msg {
2388        NautilusWsMessage::Data(payloads) => Python::attach(|py| {
2389            for data in payloads {
2390                let py_obj = data_to_pycapsule(py, data);
2391                call_python_threadsafe(py, call_soon, callback, py_obj);
2392            }
2393        }),
2394        NautilusWsMessage::Deltas(deltas) => Python::attach(|py| {
2395            let py_obj = data_to_pycapsule(py, Data::Deltas(OrderBookDeltas_API::new(deltas)));
2396            call_python_threadsafe(py, call_soon, callback, py_obj);
2397        }),
2398        NautilusWsMessage::FundingRates(updates) => {
2399            for data in updates {
2400                call_python_with_data(call_soon, callback, |py| data.into_py_any(py));
2401            }
2402        }
2403        NautilusWsMessage::Instrument(instrument, status) => {
2404            instruments_by_symbol.insert(instrument.symbol().inner(), (*instrument).clone());
2405            call_python_with_data(call_soon, callback, |py| {
2406                instrument_any_to_pyobject(py, *instrument)
2407            });
2408
2409            if let Some(status) = status {
2410                call_python_with_data(call_soon, callback, |py| status.into_py_any(py));
2411            }
2412        }
2413        NautilusWsMessage::InstrumentStatus(status) => {
2414            call_python_with_data(call_soon, callback, |py| status.into_py_any(py));
2415        }
2416        NautilusWsMessage::Raw(data) => {
2417            dispatch_json_value_to_python(&data, call_soon, callback);
2418        }
2419        _ => {}
2420    }
2421}
2422
2423fn dispatch_execution_reports_to_python(
2424    reports: Vec<ExecutionReport>,
2425    call_soon: &Py<PyAny>,
2426    callback: &Py<PyAny>,
2427) {
2428    for report in reports {
2429        match report {
2430            ExecutionReport::Order(report) => {
2431                call_python_with_data(call_soon, callback, |py| report.into_py_any(py));
2432            }
2433            ExecutionReport::Fill(report) => {
2434                call_python_with_data(call_soon, callback, |py| report.into_py_any(py));
2435            }
2436        }
2437    }
2438}