1use std::{fmt::Debug, sync::Arc};
29
30use ahash::AHashMap;
31use futures_util::StreamExt;
32use nautilus_common::{cache::quote::QuoteCache, live::get_runtime};
33use nautilus_core::{
34 AtomicMap, UUID4, UnixNanos,
35 python::{call_python_threadsafe, to_pyruntime_err, to_pyvalue_err},
36 time::get_atomic_clock_realtime,
37};
38use nautilus_model::{
39 data::{Data, InstrumentStatus, bar::BarType},
40 enums::{MarketStatusAction, OrderSide, OrderType},
41 events::{OrderAccepted, OrderUpdated},
42 identifiers::{AccountId, ClientOrderId, InstrumentId, StrategyId, TraderId, VenueOrderId},
43 instruments::{Instrument, InstrumentAny},
44 python::{
45 data::data_to_pycapsule,
46 instruments::{instrument_any_to_pyobject, pyobject_to_instrument_any},
47 },
48 types::Price,
49};
50use nautilus_network::websocket::TransportBackend;
51use pyo3::{conversion::IntoPyObjectExt, prelude::*};
52use ustr::Ustr;
53
54use crate::{
55 common::{
56 enums::{
57 BitmexEnvironment, BitmexExecType, BitmexInstrumentState, BitmexOrderType,
58 BitmexPegPriceType,
59 },
60 parse::{
61 parse_contracts_quantity, parse_instrument_id, parse_optional_datetime_to_unix_nanos,
62 },
63 },
64 http::parse::{InstrumentParseResult, parse_instrument_any},
65 websocket::{
66 BitmexWebSocketClient,
67 dispatch::{OrderIdentity, WsDispatchState, fill_report_to_order_filled},
68 enums::{BitmexAction, BitmexWsTopic},
69 messages::{
70 BitmexExecutionMsg, BitmexInstrumentMsg, BitmexQuoteMsg, BitmexTableMessage,
71 BitmexWsMessage, OrderData,
72 },
73 parse::{
74 ParsedOrderEvent, parse_book_msg_vec, parse_book10_msg_vec, parse_execution_msg,
75 parse_funding_msg, parse_instrument_msg, parse_order_event, parse_order_msg,
76 parse_order_update_msg, parse_position_msg, parse_trade_bin_msg_vec,
77 parse_trade_msg_vec, parse_wallet_msg,
78 },
79 },
80};
81
82#[pyclass(
85 name = "BitmexWebSocketClient",
86 module = "nautilus_trader.core.nautilus_pyo3.bitmex"
87)]
88#[pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.bitmex")]
89pub struct PyBitmexWebSocketClient {
90 inner: BitmexWebSocketClient,
91 instruments_cache: Arc<AtomicMap<Ustr, InstrumentAny>>,
92 ws_dispatch_state: Arc<WsDispatchState>,
93}
94
95impl Debug for PyBitmexWebSocketClient {
96 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97 f.debug_struct(stringify!(PyBitmexWebSocketClient))
98 .field("inner", &self.inner)
99 .finish_non_exhaustive()
100 }
101}
102
103#[pymethods]
104#[pyo3_stub_gen::derive::gen_stub_pymethods]
105impl PyBitmexWebSocketClient {
106 #[new]
107 #[pyo3(signature = (url=None, api_key=None, api_secret=None, account_id=None, heartbeat=5, environment=BitmexEnvironment::Mainnet, proxy_url=None))]
108 fn py_new(
109 url: Option<String>,
110 api_key: Option<String>,
111 api_secret: Option<String>,
112 account_id: Option<AccountId>,
113 heartbeat: u64,
114 environment: BitmexEnvironment,
115 proxy_url: Option<String>,
116 ) -> PyResult<Self> {
117 let inner = BitmexWebSocketClient::new_with_env(
118 url,
119 api_key,
120 api_secret,
121 account_id,
122 heartbeat,
123 environment,
124 TransportBackend::default(),
125 proxy_url,
126 )
127 .map_err(to_pyvalue_err)?;
128 Ok(Self {
129 inner,
130 instruments_cache: Arc::new(AtomicMap::new()),
131 ws_dispatch_state: Arc::new(WsDispatchState::default()),
132 })
133 }
134
135 #[staticmethod]
136 #[pyo3(name = "from_env")]
137 fn py_from_env() -> PyResult<Self> {
138 let inner = BitmexWebSocketClient::from_env().map_err(to_pyvalue_err)?;
139 Ok(Self {
140 inner,
141 instruments_cache: Arc::new(AtomicMap::new()),
142 ws_dispatch_state: Arc::new(WsDispatchState::default()),
143 })
144 }
145
146 #[getter]
147 #[pyo3(name = "url")]
148 #[must_use]
149 fn py_url(&self) -> &str {
150 self.inner.url()
151 }
152
153 #[getter]
154 #[pyo3(name = "api_key")]
155 #[must_use]
156 fn py_api_key(&self) -> Option<&str> {
157 self.inner.api_key()
158 }
159
160 #[getter]
161 #[pyo3(name = "api_key_masked")]
162 #[must_use]
163 fn py_api_key_masked(&self) -> Option<String> {
164 self.inner.api_key_masked()
165 }
166
167 #[pyo3(name = "is_active")]
168 fn py_is_active(&mut self) -> bool {
169 self.inner.is_active()
170 }
171
172 #[pyo3(name = "is_closed")]
173 fn py_is_closed(&mut self) -> bool {
174 self.inner.is_closed()
175 }
176
177 #[pyo3(name = "get_subscriptions")]
178 fn py_get_subscriptions(&self, instrument_id: InstrumentId) -> Vec<String> {
179 self.inner.get_subscriptions(instrument_id)
180 }
181
182 #[pyo3(name = "set_account_id")]
183 fn py_set_account_id(&mut self, account_id: AccountId) {
184 self.inner.set_account_id(account_id);
185 }
186
187 #[pyo3(name = "register_order_identity")]
188 fn py_register_order_identity(
189 &self,
190 client_order_id: ClientOrderId,
191 instrument_id: InstrumentId,
192 strategy_id: StrategyId,
193 order_side: OrderSide,
194 order_type: OrderType,
195 ) {
196 self.ws_dispatch_state.order_identities.insert(
197 client_order_id,
198 OrderIdentity {
199 instrument_id,
200 strategy_id,
201 order_side,
202 order_type,
203 },
204 );
205 }
206
207 #[pyo3(name = "remove_order_identity")]
208 fn py_remove_order_identity(&self, client_order_id: ClientOrderId) {
209 self.ws_dispatch_state
210 .order_identities
211 .remove(&client_order_id);
212 }
213
214 #[pyo3(name = "cache_instrument")]
215 fn py_cache_instrument(&self, py: Python, instrument: Py<PyAny>) -> PyResult<()> {
216 let inst = pyobject_to_instrument_any(py, instrument)?;
217 let symbol = inst.symbol().inner();
218 self.instruments_cache.insert(symbol, inst);
219 Ok(())
220 }
221
222 #[pyo3(name = "connect")]
223 #[pyo3(signature = (loop_, instruments, callback, trader_id=None))]
224 #[expect(clippy::needless_pass_by_value)]
225 fn py_connect<'py>(
226 &mut self,
227 py: Python<'py>,
228 loop_: Py<PyAny>,
229 instruments: Vec<Py<PyAny>>,
230 callback: Py<PyAny>,
231 trader_id: Option<TraderId>,
232 ) -> PyResult<Bound<'py, PyAny>> {
233 let call_soon: Py<PyAny> = loop_.getattr(py, "call_soon_threadsafe")?;
234
235 let cache = Arc::clone(&self.instruments_cache);
236 {
237 let mut initial: AHashMap<Ustr, InstrumentAny> = AHashMap::new();
238
239 for inst_py in instruments {
240 let inst = pyobject_to_instrument_any(py, inst_py)?;
241 initial.insert(inst.symbol().inner(), inst);
242 }
243 cache.rcu(|m| {
244 for (k, v) in &initial {
245 m.insert(*k, v.clone());
246 }
247 });
248 }
249
250 let clock = get_atomic_clock_realtime();
251 let mut client = self.inner.clone();
252 let account_id = self.inner.account_id();
253 let dispatch_state = Arc::clone(&self.ws_dispatch_state);
254 let trader_id = trader_id.unwrap_or(TraderId::from("TRADER-000"));
255
256 pyo3_async_runtimes::tokio::future_into_py(py, async move {
257 client.connect().await.map_err(to_pyruntime_err)?;
258
259 let stream = client.stream();
260
261 get_runtime().spawn(async move {
262 let _client = client; tokio::pin!(stream);
264
265 let mut quote_cache = QuoteCache::new();
266 let mut order_type_cache: AHashMap<ClientOrderId, OrderType> = AHashMap::new();
267 let mut order_symbol_cache: AHashMap<ClientOrderId, Ustr> = AHashMap::new();
268
269 while let Some(msg) = stream.next().await {
270 let ts_init = clock.get_time_ns();
271
272 match msg {
273 BitmexWsMessage::Table(table_msg) => {
274 handle_table_message(
275 table_msg,
276 &cache,
277 &mut quote_cache,
278 &mut order_type_cache,
279 &mut order_symbol_cache,
280 &dispatch_state,
281 trader_id,
282 account_id,
283 ts_init,
284 &call_soon,
285 &callback,
286 );
287 }
288 BitmexWsMessage::Reconnected => {
289 quote_cache.clear();
290 order_type_cache.clear();
291 order_symbol_cache.clear();
292 }
293 BitmexWsMessage::Authenticated => {}
294 }
295 }
296 });
297
298 Ok(())
299 })
300 }
301
302 #[pyo3(name = "wait_until_active")]
303 fn py_wait_until_active<'py>(
304 &self,
305 py: Python<'py>,
306 timeout_secs: f64,
307 ) -> PyResult<Bound<'py, PyAny>> {
308 let client = self.inner.clone();
309
310 pyo3_async_runtimes::tokio::future_into_py(py, async move {
311 client
312 .wait_until_active(timeout_secs)
313 .await
314 .map_err(to_pyruntime_err)?;
315 Ok(())
316 })
317 }
318
319 #[pyo3(name = "close")]
320 fn py_close<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
321 let mut client = self.inner.clone();
322
323 pyo3_async_runtimes::tokio::future_into_py(py, async move {
324 if let Err(e) = client.close().await {
325 log::warn!("Error on close: {e}");
326 }
327 Ok(())
328 })
329 }
330
331 #[pyo3(name = "subscribe_instruments")]
332 fn py_subscribe_instruments<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
333 let client = self.inner.clone();
334
335 pyo3_async_runtimes::tokio::future_into_py(py, async move {
336 if let Err(e) = client.subscribe_instruments().await {
337 log::error!("Failed to subscribe to instruments: {e}");
338 }
339 Ok(())
340 })
341 }
342
343 #[pyo3(name = "subscribe_instrument")]
344 fn py_subscribe_instrument<'py>(
345 &self,
346 py: Python<'py>,
347 instrument_id: InstrumentId,
348 ) -> PyResult<Bound<'py, PyAny>> {
349 let client = self.inner.clone();
350
351 pyo3_async_runtimes::tokio::future_into_py(py, async move {
352 if let Err(e) = client.subscribe_instrument(instrument_id).await {
353 log::error!("Failed to subscribe to instrument: {e}");
354 }
355 Ok(())
356 })
357 }
358
359 #[pyo3(name = "subscribe_book")]
360 fn py_subscribe_book<'py>(
361 &self,
362 py: Python<'py>,
363 instrument_id: InstrumentId,
364 ) -> PyResult<Bound<'py, PyAny>> {
365 let client = self.inner.clone();
366
367 pyo3_async_runtimes::tokio::future_into_py(py, async move {
368 if let Err(e) = client.subscribe_book(instrument_id).await {
369 log::error!("Failed to subscribe to order book: {e}");
370 }
371 Ok(())
372 })
373 }
374
375 #[pyo3(name = "subscribe_book_25")]
376 fn py_subscribe_book_25<'py>(
377 &self,
378 py: Python<'py>,
379 instrument_id: InstrumentId,
380 ) -> PyResult<Bound<'py, PyAny>> {
381 let client = self.inner.clone();
382
383 pyo3_async_runtimes::tokio::future_into_py(py, async move {
384 if let Err(e) = client.subscribe_book_25(instrument_id).await {
385 log::error!("Failed to subscribe to order book 25: {e}");
386 }
387 Ok(())
388 })
389 }
390
391 #[pyo3(name = "subscribe_book_depth10")]
392 fn py_subscribe_book_depth10<'py>(
393 &self,
394 py: Python<'py>,
395 instrument_id: InstrumentId,
396 ) -> PyResult<Bound<'py, PyAny>> {
397 let client = self.inner.clone();
398
399 pyo3_async_runtimes::tokio::future_into_py(py, async move {
400 if let Err(e) = client.subscribe_book_depth10(instrument_id).await {
401 log::error!("Failed to subscribe to order book depth 10: {e}");
402 }
403 Ok(())
404 })
405 }
406
407 #[pyo3(name = "subscribe_quotes")]
408 fn py_subscribe_quotes<'py>(
409 &self,
410 py: Python<'py>,
411 instrument_id: InstrumentId,
412 ) -> PyResult<Bound<'py, PyAny>> {
413 let client = self.inner.clone();
414
415 pyo3_async_runtimes::tokio::future_into_py(py, async move {
416 if let Err(e) = client.subscribe_quotes(instrument_id).await {
417 log::error!("Failed to subscribe to quotes: {e}");
418 }
419 Ok(())
420 })
421 }
422
423 #[pyo3(name = "subscribe_trades")]
424 fn py_subscribe_trades<'py>(
425 &self,
426 py: Python<'py>,
427 instrument_id: InstrumentId,
428 ) -> PyResult<Bound<'py, PyAny>> {
429 let client = self.inner.clone();
430
431 pyo3_async_runtimes::tokio::future_into_py(py, async move {
432 if let Err(e) = client.subscribe_trades(instrument_id).await {
433 log::error!("Failed to subscribe to trades: {e}");
434 }
435 Ok(())
436 })
437 }
438
439 #[pyo3(name = "subscribe_mark_prices")]
440 fn py_subscribe_mark_prices<'py>(
441 &self,
442 py: Python<'py>,
443 instrument_id: InstrumentId,
444 ) -> PyResult<Bound<'py, PyAny>> {
445 let client = self.inner.clone();
446
447 pyo3_async_runtimes::tokio::future_into_py(py, async move {
448 if let Err(e) = client.subscribe_mark_prices(instrument_id).await {
449 log::error!("Failed to subscribe to mark prices: {e}");
450 }
451 Ok(())
452 })
453 }
454
455 #[pyo3(name = "subscribe_index_prices")]
456 fn py_subscribe_index_prices<'py>(
457 &self,
458 py: Python<'py>,
459 instrument_id: InstrumentId,
460 ) -> PyResult<Bound<'py, PyAny>> {
461 let client = self.inner.clone();
462
463 pyo3_async_runtimes::tokio::future_into_py(py, async move {
464 if let Err(e) = client.subscribe_index_prices(instrument_id).await {
465 log::error!("Failed to subscribe to index prices: {e}");
466 }
467 Ok(())
468 })
469 }
470
471 #[pyo3(name = "subscribe_funding_rates")]
472 fn py_subscribe_funding_rates<'py>(
473 &self,
474 py: Python<'py>,
475 instrument_id: InstrumentId,
476 ) -> PyResult<Bound<'py, PyAny>> {
477 let client = self.inner.clone();
478
479 pyo3_async_runtimes::tokio::future_into_py(py, async move {
480 if let Err(e) = client.subscribe_funding_rates(instrument_id).await {
481 log::error!("Failed to subscribe to funding: {e}");
482 }
483 Ok(())
484 })
485 }
486
487 #[pyo3(name = "subscribe_bars")]
488 fn py_subscribe_bars<'py>(
489 &self,
490 py: Python<'py>,
491 bar_type: BarType,
492 ) -> PyResult<Bound<'py, PyAny>> {
493 let client = self.inner.clone();
494
495 pyo3_async_runtimes::tokio::future_into_py(py, async move {
496 if let Err(e) = client.subscribe_bars(bar_type).await {
497 log::error!("Failed to subscribe to bars: {e}");
498 }
499 Ok(())
500 })
501 }
502
503 #[pyo3(name = "unsubscribe_instruments")]
504 fn py_unsubscribe_instruments<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
505 let client = self.inner.clone();
506
507 pyo3_async_runtimes::tokio::future_into_py(py, async move {
508 if let Err(e) = client.unsubscribe_instruments().await {
509 log::error!("Failed to unsubscribe from instruments: {e}");
510 }
511 Ok(())
512 })
513 }
514
515 #[pyo3(name = "unsubscribe_instrument")]
516 fn py_unsubscribe_instrument<'py>(
517 &self,
518 py: Python<'py>,
519 instrument_id: InstrumentId,
520 ) -> PyResult<Bound<'py, PyAny>> {
521 let client = self.inner.clone();
522
523 pyo3_async_runtimes::tokio::future_into_py(py, async move {
524 if let Err(e) = client.unsubscribe_instrument(instrument_id).await {
525 log::error!("Failed to unsubscribe from instrument: {e}");
526 }
527 Ok(())
528 })
529 }
530
531 #[pyo3(name = "unsubscribe_book")]
532 fn py_unsubscribe_book<'py>(
533 &self,
534 py: Python<'py>,
535 instrument_id: InstrumentId,
536 ) -> PyResult<Bound<'py, PyAny>> {
537 let client = self.inner.clone();
538
539 pyo3_async_runtimes::tokio::future_into_py(py, async move {
540 if let Err(e) = client.unsubscribe_book(instrument_id).await {
541 log::error!("Failed to unsubscribe from order book: {e}");
542 }
543 Ok(())
544 })
545 }
546
547 #[pyo3(name = "unsubscribe_book_25")]
548 fn py_unsubscribe_book_25<'py>(
549 &self,
550 py: Python<'py>,
551 instrument_id: InstrumentId,
552 ) -> PyResult<Bound<'py, PyAny>> {
553 let client = self.inner.clone();
554
555 pyo3_async_runtimes::tokio::future_into_py(py, async move {
556 if let Err(e) = client.unsubscribe_book_25(instrument_id).await {
557 log::error!("Failed to unsubscribe from order book 25: {e}");
558 }
559 Ok(())
560 })
561 }
562
563 #[pyo3(name = "unsubscribe_book_depth10")]
564 fn py_unsubscribe_book_depth10<'py>(
565 &self,
566 py: Python<'py>,
567 instrument_id: InstrumentId,
568 ) -> PyResult<Bound<'py, PyAny>> {
569 let client = self.inner.clone();
570
571 pyo3_async_runtimes::tokio::future_into_py(py, async move {
572 if let Err(e) = client.unsubscribe_book_depth10(instrument_id).await {
573 log::error!("Failed to unsubscribe from order book depth 10: {e}");
574 }
575 Ok(())
576 })
577 }
578
579 #[pyo3(name = "unsubscribe_quotes")]
580 fn py_unsubscribe_quotes<'py>(
581 &self,
582 py: Python<'py>,
583 instrument_id: InstrumentId,
584 ) -> PyResult<Bound<'py, PyAny>> {
585 let client = self.inner.clone();
586
587 pyo3_async_runtimes::tokio::future_into_py(py, async move {
588 if let Err(e) = client.unsubscribe_quotes(instrument_id).await {
589 log::error!("Failed to unsubscribe from quotes: {e}");
590 }
591 Ok(())
592 })
593 }
594
595 #[pyo3(name = "unsubscribe_trades")]
596 fn py_unsubscribe_trades<'py>(
597 &self,
598 py: Python<'py>,
599 instrument_id: InstrumentId,
600 ) -> PyResult<Bound<'py, PyAny>> {
601 let client = self.inner.clone();
602
603 pyo3_async_runtimes::tokio::future_into_py(py, async move {
604 if let Err(e) = client.unsubscribe_trades(instrument_id).await {
605 log::error!("Failed to unsubscribe from trades: {e}");
606 }
607 Ok(())
608 })
609 }
610
611 #[pyo3(name = "unsubscribe_mark_prices")]
612 fn py_unsubscribe_mark_prices<'py>(
613 &self,
614 py: Python<'py>,
615 instrument_id: InstrumentId,
616 ) -> PyResult<Bound<'py, PyAny>> {
617 let client = self.inner.clone();
618
619 pyo3_async_runtimes::tokio::future_into_py(py, async move {
620 if let Err(e) = client.unsubscribe_mark_prices(instrument_id).await {
621 log::error!("Failed to unsubscribe from mark prices: {e}");
622 }
623 Ok(())
624 })
625 }
626
627 #[pyo3(name = "unsubscribe_index_prices")]
628 fn py_unsubscribe_index_prices<'py>(
629 &self,
630 py: Python<'py>,
631 instrument_id: InstrumentId,
632 ) -> PyResult<Bound<'py, PyAny>> {
633 let client = self.inner.clone();
634
635 pyo3_async_runtimes::tokio::future_into_py(py, async move {
636 if let Err(e) = client.unsubscribe_index_prices(instrument_id).await {
637 log::error!("Failed to unsubscribe from index prices: {e}");
638 }
639 Ok(())
640 })
641 }
642
643 #[pyo3(name = "unsubscribe_funding_rates")]
644 fn py_unsubscribe_funding_rates<'py>(
645 &self,
646 py: Python<'py>,
647 instrument_id: InstrumentId,
648 ) -> PyResult<Bound<'py, PyAny>> {
649 let client = self.inner.clone();
650 pyo3_async_runtimes::tokio::future_into_py(py, async move {
651 if let Err(e) = client.unsubscribe_funding_rates(instrument_id).await {
652 log::error!("Failed to unsubscribe from funding rates: {e}");
653 }
654 Ok(())
655 })
656 }
657
658 #[pyo3(name = "unsubscribe_bars")]
659 fn py_unsubscribe_bars<'py>(
660 &self,
661 py: Python<'py>,
662 bar_type: BarType,
663 ) -> PyResult<Bound<'py, PyAny>> {
664 let client = self.inner.clone();
665
666 pyo3_async_runtimes::tokio::future_into_py(py, async move {
667 if let Err(e) = client.unsubscribe_bars(bar_type).await {
668 log::error!("Failed to unsubscribe from bars: {e}");
669 }
670 Ok(())
671 })
672 }
673
674 #[pyo3(name = "subscribe_orders")]
675 fn py_subscribe_orders<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
676 let client = self.inner.clone();
677
678 pyo3_async_runtimes::tokio::future_into_py(py, async move {
679 if let Err(e) = client.subscribe_orders().await {
680 log::error!("Failed to subscribe to orders: {e}");
681 }
682 Ok(())
683 })
684 }
685
686 #[pyo3(name = "subscribe_executions")]
687 fn py_subscribe_executions<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
688 let client = self.inner.clone();
689
690 pyo3_async_runtimes::tokio::future_into_py(py, async move {
691 if let Err(e) = client.subscribe_executions().await {
692 log::error!("Failed to subscribe to executions: {e}");
693 }
694 Ok(())
695 })
696 }
697
698 #[pyo3(name = "subscribe_positions")]
699 fn py_subscribe_positions<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
700 let client = self.inner.clone();
701
702 pyo3_async_runtimes::tokio::future_into_py(py, async move {
703 if let Err(e) = client.subscribe_positions().await {
704 log::error!("Failed to subscribe to positions: {e}");
705 }
706 Ok(())
707 })
708 }
709
710 #[pyo3(name = "subscribe_margin")]
711 fn py_subscribe_margin<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
712 let client = self.inner.clone();
713
714 pyo3_async_runtimes::tokio::future_into_py(py, async move {
715 if let Err(e) = client.subscribe_margin().await {
716 log::error!("Failed to subscribe to margin: {e}");
717 }
718 Ok(())
719 })
720 }
721
722 #[pyo3(name = "subscribe_wallet")]
723 fn py_subscribe_wallet<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
724 let client = self.inner.clone();
725
726 pyo3_async_runtimes::tokio::future_into_py(py, async move {
727 if let Err(e) = client.subscribe_wallet().await {
728 log::error!("Failed to subscribe to wallet: {e}");
729 }
730 Ok(())
731 })
732 }
733
734 #[pyo3(name = "unsubscribe_orders")]
735 fn py_unsubscribe_orders<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
736 let client = self.inner.clone();
737
738 pyo3_async_runtimes::tokio::future_into_py(py, async move {
739 if let Err(e) = client.unsubscribe_orders().await {
740 log::error!("Failed to unsubscribe from orders: {e}");
741 }
742 Ok(())
743 })
744 }
745
746 #[pyo3(name = "unsubscribe_executions")]
747 fn py_unsubscribe_executions<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
748 let client = self.inner.clone();
749
750 pyo3_async_runtimes::tokio::future_into_py(py, async move {
751 if let Err(e) = client.unsubscribe_executions().await {
752 log::error!("Failed to unsubscribe from executions: {e}");
753 }
754 Ok(())
755 })
756 }
757
758 #[pyo3(name = "unsubscribe_positions")]
759 fn py_unsubscribe_positions<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
760 let client = self.inner.clone();
761
762 pyo3_async_runtimes::tokio::future_into_py(py, async move {
763 if let Err(e) = client.unsubscribe_positions().await {
764 log::error!("Failed to unsubscribe from positions: {e}");
765 }
766 Ok(())
767 })
768 }
769
770 #[pyo3(name = "unsubscribe_margin")]
771 fn py_unsubscribe_margin<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
772 let client = self.inner.clone();
773
774 pyo3_async_runtimes::tokio::future_into_py(py, async move {
775 if let Err(e) = client.unsubscribe_margin().await {
776 log::error!("Failed to unsubscribe from margin: {e}");
777 }
778 Ok(())
779 })
780 }
781
782 #[pyo3(name = "unsubscribe_wallet")]
783 fn py_unsubscribe_wallet<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
784 let client = self.inner.clone();
785
786 pyo3_async_runtimes::tokio::future_into_py(py, async move {
787 if let Err(e) = client.unsubscribe_wallet().await {
788 log::error!("Failed to unsubscribe from wallet: {e}");
789 }
790 Ok(())
791 })
792 }
793}
794
795#[expect(clippy::too_many_arguments)]
796fn handle_table_message(
797 table_msg: BitmexTableMessage,
798 instruments_cache: &Arc<AtomicMap<Ustr, InstrumentAny>>,
799 quote_cache: &mut QuoteCache,
800 order_type_cache: &mut AHashMap<ClientOrderId, OrderType>,
801 order_symbol_cache: &mut AHashMap<ClientOrderId, Ustr>,
802 dispatch_state: &WsDispatchState,
803 trader_id: TraderId,
804 account_id: AccountId,
805 ts_init: UnixNanos,
806 call_soon: &Py<PyAny>,
807 callback: &Py<PyAny>,
808) {
809 if let BitmexTableMessage::Instrument { action, data } = table_msg {
810 handle_instrument_messages(
811 action,
812 data,
813 instruments_cache,
814 ts_init,
815 call_soon,
816 callback,
817 );
818 return;
819 }
820
821 let instruments = instruments_cache.load();
822
823 match table_msg {
824 BitmexTableMessage::OrderBookL2 { action, data }
825 | BitmexTableMessage::OrderBookL2_25 { action, data } => {
826 if !data.is_empty() {
827 for d in parse_book_msg_vec(data, action, &instruments, ts_init) {
828 send_data_to_python(d, call_soon, callback);
829 }
830 }
831 }
832 BitmexTableMessage::OrderBook10 { data, .. } => {
833 if !data.is_empty() {
834 for d in parse_book10_msg_vec(data, &instruments, ts_init) {
835 send_data_to_python(d, call_soon, callback);
836 }
837 }
838 }
839 BitmexTableMessage::Quote { data, .. } => {
840 handle_quote_messages(
841 data,
842 &instruments,
843 quote_cache,
844 ts_init,
845 call_soon,
846 callback,
847 );
848 }
849 BitmexTableMessage::Trade { data, .. } => {
850 if !data.is_empty() {
851 for d in parse_trade_msg_vec(data, &instruments, ts_init) {
852 send_data_to_python(d, call_soon, callback);
853 }
854 }
855 }
856 BitmexTableMessage::TradeBin1m { action, data } => {
857 if action != BitmexAction::Partial && !data.is_empty() {
858 for d in
859 parse_trade_bin_msg_vec(data, &BitmexWsTopic::TradeBin1m, &instruments, ts_init)
860 {
861 send_data_to_python(d, call_soon, callback);
862 }
863 }
864 }
865 BitmexTableMessage::TradeBin5m { action, data } => {
866 if action != BitmexAction::Partial && !data.is_empty() {
867 for d in
868 parse_trade_bin_msg_vec(data, &BitmexWsTopic::TradeBin5m, &instruments, ts_init)
869 {
870 send_data_to_python(d, call_soon, callback);
871 }
872 }
873 }
874 BitmexTableMessage::TradeBin1h { action, data } => {
875 if action != BitmexAction::Partial && !data.is_empty() {
876 for d in
877 parse_trade_bin_msg_vec(data, &BitmexWsTopic::TradeBin1h, &instruments, ts_init)
878 {
879 send_data_to_python(d, call_soon, callback);
880 }
881 }
882 }
883 BitmexTableMessage::TradeBin1d { action, data } => {
884 if action != BitmexAction::Partial && !data.is_empty() {
885 for d in
886 parse_trade_bin_msg_vec(data, &BitmexWsTopic::TradeBin1d, &instruments, ts_init)
887 {
888 send_data_to_python(d, call_soon, callback);
889 }
890 }
891 }
892 BitmexTableMessage::Funding { data, .. } => {
893 for msg in data {
894 send_to_python(parse_funding_msg(&msg, ts_init), call_soon, callback);
895 }
896 }
897 BitmexTableMessage::Order { data, .. } => {
898 handle_order_messages(
899 data,
900 &instruments,
901 order_type_cache,
902 order_symbol_cache,
903 dispatch_state,
904 trader_id,
905 account_id,
906 ts_init,
907 call_soon,
908 callback,
909 );
910 }
911 BitmexTableMessage::Execution { data, .. } => {
912 handle_execution_messages(
913 data,
914 &instruments,
915 order_symbol_cache,
916 dispatch_state,
917 trader_id,
918 account_id,
919 ts_init,
920 call_soon,
921 callback,
922 );
923 }
924 BitmexTableMessage::Position { data, .. } => {
925 for msg in data {
926 let Some(instrument) = instruments.get(&msg.symbol) else {
927 log::warn!("Instrument cache miss for position symbol={}", msg.symbol);
928 continue;
929 };
930
931 let mut report = parse_position_msg(&msg, instrument, ts_init);
932 report.account_id = account_id;
933 send_to_python(report, call_soon, callback);
934 }
935 }
936 BitmexTableMessage::Wallet { data, .. } => {
937 for msg in data {
938 let mut account_state = parse_wallet_msg(&msg, ts_init);
939 account_state.account_id = account_id;
940 send_to_python(account_state, call_soon, callback);
941 }
942 }
943 BitmexTableMessage::Margin { .. } => {}
944 _ => {
945 log::debug!("Unhandled table message type in Python WebSocket client");
946 }
947 }
948}
949
950fn handle_quote_messages(
951 data: Vec<BitmexQuoteMsg>,
952 instruments: &AHashMap<Ustr, InstrumentAny>,
953 quote_cache: &mut QuoteCache,
954 ts_init: UnixNanos,
955 call_soon: &Py<PyAny>,
956 callback: &Py<PyAny>,
957) {
958 for msg in data {
959 let Some(instrument) = instruments.get(&msg.symbol) else {
960 log::error!(
961 "Instrument cache miss: quote dropped for symbol={}",
962 msg.symbol,
963 );
964 continue;
965 };
966
967 let instrument_id = instrument.id();
968 let price_precision = instrument.price_precision();
969
970 let bid_price = msg.bid_price.map(|p| Price::new(p, price_precision));
971 let ask_price = msg.ask_price.map(|p| Price::new(p, price_precision));
972 let bid_size = msg
973 .bid_size
974 .map(|s| parse_contracts_quantity(s, instrument));
975 let ask_size = msg
976 .ask_size
977 .map(|s| parse_contracts_quantity(s, instrument));
978 let ts_event = UnixNanos::from(msg.timestamp);
979
980 match quote_cache.process(
981 instrument_id,
982 bid_price,
983 ask_price,
984 bid_size,
985 ask_size,
986 ts_event,
987 ts_init,
988 ) {
989 Ok(quote) => send_data_to_python(Data::Quote(quote), call_soon, callback),
990 Err(e) => {
991 log::warn!("Failed to process quote for {}: {e}", msg.symbol);
992 }
993 }
994 }
995}
996
997fn handle_instrument_messages(
998 action: BitmexAction,
999 data: Vec<BitmexInstrumentMsg>,
1000 instruments_cache: &Arc<AtomicMap<Ustr, InstrumentAny>>,
1001 ts_init: UnixNanos,
1002 call_soon: &Py<PyAny>,
1003 callback: &Py<PyAny>,
1004) {
1005 if action == BitmexAction::Partial || action == BitmexAction::Insert {
1006 let data_for_prices = data.clone();
1007
1008 let mut new_instruments: Vec<(Ustr, InstrumentAny)> = Vec::new();
1009
1010 for msg in data {
1011 match msg.try_into() {
1012 Ok(http_inst) => match parse_instrument_any(&http_inst, ts_init) {
1013 InstrumentParseResult::Ok(boxed) => {
1014 let inst = *boxed;
1015 let symbol = inst.symbol().inner();
1016 new_instruments.push((symbol, inst));
1017 }
1018 InstrumentParseResult::Unsupported { .. }
1019 | InstrumentParseResult::Inactive { .. } => {}
1020 InstrumentParseResult::Failed { symbol, error, .. } => {
1021 log::warn!("Failed to parse instrument {symbol}: {error}");
1022 }
1023 },
1024 Err(e) => {
1025 log::debug!("Skipping instrument (missing required fields): {e}");
1026 }
1027 }
1028 }
1029
1030 instruments_cache.rcu(|m| {
1031 for (symbol, inst) in &new_instruments {
1032 m.insert(*symbol, inst.clone());
1033 }
1034 });
1035
1036 for (_, inst) in &new_instruments {
1037 Python::attach(|py| {
1038 if let Ok(py_obj) = instrument_any_to_pyobject(py, inst.clone()) {
1039 call_python_threadsafe(py, call_soon, callback, py_obj);
1040 }
1041 });
1042 }
1043
1044 let cache = instruments_cache.load();
1045 for msg in data_for_prices {
1046 for d in parse_instrument_msg(&msg, &cache, ts_init) {
1047 send_data_to_python(d, call_soon, callback);
1048 }
1049 }
1050 } else {
1051 for msg in &data {
1052 if let Some(state_str) = &msg.state
1053 && let Ok(state) =
1054 serde_json::from_str::<BitmexInstrumentState>(&format!("\"{state_str}\""))
1055 {
1056 let instrument_id = parse_instrument_id(msg.symbol);
1057 let action = MarketStatusAction::from(&state);
1058 let is_trading = Some(state == BitmexInstrumentState::Open);
1059 let ts_event =
1060 parse_optional_datetime_to_unix_nanos(&Some(msg.timestamp), "timestamp");
1061 let status = InstrumentStatus::new(
1062 instrument_id,
1063 action,
1064 ts_event,
1065 ts_init,
1066 None,
1067 None,
1068 is_trading,
1069 None,
1070 None,
1071 );
1072 send_to_python(status, call_soon, callback);
1073 }
1074 }
1075
1076 let cache = instruments_cache.load();
1077 for msg in data {
1078 for d in parse_instrument_msg(&msg, &cache, ts_init) {
1079 send_data_to_python(d, call_soon, callback);
1080 }
1081 }
1082 }
1083}
1084
1085#[expect(clippy::too_many_arguments)]
1086fn handle_order_messages(
1087 data: Vec<OrderData>,
1088 instruments: &AHashMap<Ustr, InstrumentAny>,
1089 order_type_cache: &mut AHashMap<ClientOrderId, OrderType>,
1090 order_symbol_cache: &mut AHashMap<ClientOrderId, Ustr>,
1091 dispatch_state: &WsDispatchState,
1092 trader_id: TraderId,
1093 account_id: AccountId,
1094 ts_init: UnixNanos,
1095 call_soon: &Py<PyAny>,
1096 callback: &Py<PyAny>,
1097) {
1098 for order_data in data {
1099 match order_data {
1100 OrderData::Full(order_msg) => {
1101 let Some(instrument) = instruments.get(&order_msg.symbol) else {
1102 log::warn!(
1103 "Instrument cache miss for order symbol={}",
1104 order_msg.symbol
1105 );
1106 continue;
1107 };
1108
1109 let client_order_id = order_msg.cl_ord_id.map(ClientOrderId::new);
1110
1111 if let Some(ref cid) = client_order_id {
1112 if let Some(ord_type) = &order_msg.ord_type {
1113 let order_type: OrderType = if *ord_type == BitmexOrderType::Pegged
1114 && order_msg.peg_price_type == Some(BitmexPegPriceType::TrailingStopPeg)
1115 {
1116 if order_msg.price.is_some() {
1117 OrderType::TrailingStopLimit
1118 } else {
1119 OrderType::TrailingStopMarket
1120 }
1121 } else {
1122 (*ord_type).into()
1123 };
1124 order_type_cache.insert(*cid, order_type);
1125 }
1126 order_symbol_cache.insert(*cid, order_msg.symbol);
1127 }
1128
1129 let identity = client_order_id.and_then(|cid| {
1130 dispatch_state
1131 .order_identities
1132 .get(&cid)
1133 .map(|r| (cid, r.clone()))
1134 });
1135
1136 if let Some((cid, ident)) = identity {
1137 if let Some(event) = parse_order_event(
1138 &order_msg,
1139 cid,
1140 account_id,
1141 trader_id,
1142 ident.strategy_id,
1143 ts_init,
1144 ) {
1145 let venue_order_id = VenueOrderId::new(order_msg.order_id.to_string());
1146 dispatch_order_event_to_python(
1147 event,
1148 cid,
1149 account_id,
1150 venue_order_id,
1151 &ident,
1152 dispatch_state,
1153 trader_id,
1154 ts_init,
1155 call_soon,
1156 callback,
1157 );
1158 }
1159
1160 if order_msg.ord_status.is_terminal() {
1161 order_type_cache.remove(&cid);
1162 order_symbol_cache.remove(&cid);
1163 }
1164 } else {
1165 match parse_order_msg(&order_msg, instrument, order_type_cache, ts_init) {
1166 Ok(mut report) => {
1167 if report.order_status.is_closed()
1168 && let Some(cid) = report.client_order_id
1169 {
1170 order_type_cache.remove(&cid);
1171 order_symbol_cache.remove(&cid);
1172 }
1173 report.account_id = account_id;
1174 send_to_python(report, call_soon, callback);
1175 }
1176 Err(e) => log::error!("Failed to parse order message: {e}"),
1177 }
1178 }
1179 }
1180 OrderData::Update(msg) => {
1181 if let Some(cl_ord_id) = &msg.cl_ord_id {
1182 let cid = ClientOrderId::new(cl_ord_id);
1183 order_symbol_cache.insert(cid, msg.symbol);
1184 }
1185
1186 let Some(instrument) = instruments.get(&msg.symbol) else {
1187 log::warn!(
1188 "Instrument cache miss for order update symbol={}",
1189 msg.symbol,
1190 );
1191 continue;
1192 };
1193
1194 let identity = msg.cl_ord_id.as_ref().and_then(|cl| {
1195 let cid = ClientOrderId::new(cl);
1196 dispatch_state
1197 .order_identities
1198 .get(&cid)
1199 .map(|r| (cid, r.clone()))
1200 });
1201
1202 if let Some((cid, ident)) = identity {
1203 if let Some(event) =
1204 parse_order_update_msg(&msg, instrument, account_id, ts_init)
1205 {
1206 let enriched = OrderUpdated::new(
1207 trader_id,
1208 ident.strategy_id,
1209 event.instrument_id,
1210 cid,
1211 event.quantity,
1212 event.event_id,
1213 event.ts_event,
1214 event.ts_init,
1215 false,
1216 event.venue_order_id,
1217 Some(account_id),
1218 event.price,
1219 event.trigger_price,
1220 event.protection_price,
1221 false, );
1223 let venue_order_id = enriched
1224 .venue_order_id
1225 .unwrap_or_else(|| VenueOrderId::new(msg.order_id.to_string()));
1226 ensure_accepted_to_python(
1227 cid,
1228 account_id,
1229 venue_order_id,
1230 &ident,
1231 dispatch_state,
1232 trader_id,
1233 ts_init,
1234 call_soon,
1235 callback,
1236 );
1237 send_to_python(enriched, call_soon, callback);
1238 }
1239 } else {
1240 log::debug!(
1241 "Skipping order update for untracked order: order_id={}",
1242 msg.order_id,
1243 );
1244 }
1245 }
1246 }
1247 }
1248}
1249
1250#[expect(clippy::too_many_arguments)]
1251fn handle_execution_messages(
1252 data: Vec<BitmexExecutionMsg>,
1253 instruments: &AHashMap<Ustr, InstrumentAny>,
1254 order_symbol_cache: &AHashMap<ClientOrderId, Ustr>,
1255 dispatch_state: &WsDispatchState,
1256 trader_id: TraderId,
1257 account_id: AccountId,
1258 ts_init: UnixNanos,
1259 call_soon: &Py<PyAny>,
1260 callback: &Py<PyAny>,
1261) {
1262 for exec_msg in data {
1263 let symbol = exec_msg.symbol.or_else(|| {
1264 exec_msg
1265 .cl_ord_id
1266 .map(ClientOrderId::new)
1267 .and_then(|cid| order_symbol_cache.get(&cid).copied())
1268 });
1269
1270 let Some(symbol) = symbol else {
1271 if let Some(cl_ord_id) = &exec_msg.cl_ord_id {
1272 if exec_msg.exec_type == Some(BitmexExecType::Trade) {
1273 log::warn!(
1274 "Execution missing symbol and not in cache: \
1275 cl_ord_id={cl_ord_id}, exec_id={:?}",
1276 exec_msg.exec_id,
1277 );
1278 } else {
1279 log::debug!(
1280 "Execution missing symbol and not in cache: \
1281 cl_ord_id={cl_ord_id}, exec_type={:?}",
1282 exec_msg.exec_type,
1283 );
1284 }
1285 } else if exec_msg.exec_type == Some(BitmexExecType::CancelReject) {
1286 log::debug!(
1287 "CancelReject missing symbol/clOrdID: exec_id={:?}, order_id={:?}",
1288 exec_msg.exec_id,
1289 exec_msg.order_id,
1290 );
1291 } else {
1292 log::warn!(
1293 "Execution missing both symbol and clOrdID: \
1294 exec_id={:?}, order_id={:?}, exec_type={:?}",
1295 exec_msg.exec_id,
1296 exec_msg.order_id,
1297 exec_msg.exec_type,
1298 );
1299 }
1300 continue;
1301 };
1302
1303 let Some(instrument) = instruments.get(&symbol) else {
1304 log::warn!("Instrument cache miss for execution symbol={symbol}");
1305 continue;
1306 };
1307
1308 let Some(mut fill) = parse_execution_msg(exec_msg, instrument, ts_init) else {
1309 continue;
1310 };
1311 fill.account_id = account_id;
1312
1313 let identity = fill.client_order_id.and_then(|cid| {
1314 dispatch_state
1315 .order_identities
1316 .get(&cid)
1317 .map(|r| (cid, r.clone()))
1318 });
1319
1320 if let Some((cid, ident)) = identity {
1321 let venue_order_id = fill.venue_order_id;
1322 ensure_accepted_to_python(
1323 cid,
1324 fill.account_id,
1325 venue_order_id,
1326 &ident,
1327 dispatch_state,
1328 trader_id,
1329 ts_init,
1330 call_soon,
1331 callback,
1332 );
1333 dispatch_state.insert_filled(cid);
1334 dispatch_state.remove_triggered(&cid);
1335 let filled =
1336 fill_report_to_order_filled(&fill, trader_id, &ident, instrument.quote_currency());
1337 send_to_python(filled, call_soon, callback);
1338 } else {
1339 send_to_python(fill, call_soon, callback);
1340 }
1341 }
1342}
1343
1344#[expect(clippy::too_many_arguments, clippy::needless_pass_by_value)]
1346fn dispatch_order_event_to_python(
1347 event: ParsedOrderEvent,
1348 client_order_id: ClientOrderId,
1349 account_id: AccountId,
1350 venue_order_id: VenueOrderId,
1351 identity: &OrderIdentity,
1352 state: &WsDispatchState,
1353 trader_id: TraderId,
1354 ts_init: UnixNanos,
1355 call_soon: &Py<PyAny>,
1356 callback: &Py<PyAny>,
1357) {
1358 let is_terminal;
1359
1360 match event {
1361 ParsedOrderEvent::Accepted(e) => {
1362 if state.accepted_contains(&client_order_id)
1363 || state.filled_contains(&client_order_id)
1364 || state.triggered_contains(&client_order_id)
1365 {
1366 log::debug!("Skipping duplicate Accepted for {client_order_id}");
1367 return;
1368 }
1369 state.insert_accepted(client_order_id);
1370 is_terminal = false;
1371 send_to_python(e, call_soon, callback);
1372 }
1373 ParsedOrderEvent::Triggered(e) => {
1374 if state.filled_contains(&client_order_id) {
1375 log::debug!("Skipping stale Triggered for {client_order_id} (already filled)");
1376 return;
1377 }
1378 ensure_accepted_to_python(
1379 client_order_id,
1380 account_id,
1381 venue_order_id,
1382 identity,
1383 state,
1384 trader_id,
1385 ts_init,
1386 call_soon,
1387 callback,
1388 );
1389 state.insert_triggered(client_order_id);
1390 is_terminal = false;
1391 send_to_python(e, call_soon, callback);
1392 }
1393 ParsedOrderEvent::Canceled(e) => {
1394 ensure_accepted_to_python(
1395 client_order_id,
1396 account_id,
1397 venue_order_id,
1398 identity,
1399 state,
1400 trader_id,
1401 ts_init,
1402 call_soon,
1403 callback,
1404 );
1405 state.remove_triggered(&client_order_id);
1406 state.remove_filled(&client_order_id);
1407 is_terminal = true;
1408 send_to_python(e, call_soon, callback);
1409 }
1410 ParsedOrderEvent::Expired(e) => {
1411 ensure_accepted_to_python(
1412 client_order_id,
1413 account_id,
1414 venue_order_id,
1415 identity,
1416 state,
1417 trader_id,
1418 ts_init,
1419 call_soon,
1420 callback,
1421 );
1422 state.remove_triggered(&client_order_id);
1423 state.remove_filled(&client_order_id);
1424 is_terminal = true;
1425 send_to_python(e, call_soon, callback);
1426 }
1427 ParsedOrderEvent::Rejected(e) => {
1428 state.remove_triggered(&client_order_id);
1429 state.remove_filled(&client_order_id);
1430 is_terminal = true;
1431 send_to_python(e, call_soon, callback);
1432 }
1433 }
1434
1435 if is_terminal {
1436 state.order_identities.remove(&client_order_id);
1437 state.remove_accepted(&client_order_id);
1438 }
1439}
1440
1441#[expect(clippy::too_many_arguments)]
1443fn ensure_accepted_to_python(
1444 client_order_id: ClientOrderId,
1445 account_id: AccountId,
1446 venue_order_id: VenueOrderId,
1447 identity: &OrderIdentity,
1448 state: &WsDispatchState,
1449 trader_id: TraderId,
1450 ts_init: UnixNanos,
1451 call_soon: &Py<PyAny>,
1452 callback: &Py<PyAny>,
1453) {
1454 if state.accepted_contains(&client_order_id) {
1455 return;
1456 }
1457 state.insert_accepted(client_order_id);
1458 let accepted = OrderAccepted::new(
1459 trader_id,
1460 identity.strategy_id,
1461 identity.instrument_id,
1462 client_order_id,
1463 venue_order_id,
1464 account_id,
1465 UUID4::new(),
1466 ts_init,
1467 ts_init,
1468 false,
1469 );
1470 send_to_python(accepted, call_soon, callback);
1471}
1472
1473fn send_data_to_python(data: Data, call_soon: &Py<PyAny>, callback: &Py<PyAny>) {
1474 Python::attach(|py| {
1475 let py_obj = data_to_pycapsule(py, data);
1476 call_python_threadsafe(py, call_soon, callback, py_obj);
1477 });
1478}
1479
1480fn send_to_python<T: for<'py> IntoPyObjectExt<'py>>(
1481 value: T,
1482 call_soon: &Py<PyAny>,
1483 callback: &Py<PyAny>,
1484) {
1485 Python::attach(|py| {
1486 if let Ok(py_obj) = value.into_py_any(py) {
1487 call_python_threadsafe(py, call_soon, callback, py_obj);
1488 }
1489 });
1490}