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