1use std::sync::{
45 Arc,
46 atomic::{AtomicU64, Ordering},
47};
48
49use ahash::AHashMap;
50use futures_util::StreamExt;
51use nautilus_common::live::get_runtime;
52use nautilus_core::{
53 AtomicMap, UnixNanos,
54 python::{call_python_threadsafe, to_pyruntime_err, to_pyvalue_err},
55 time::get_atomic_clock_realtime,
56};
57use nautilus_model::{
58 data::{BarType, Data, OrderBookDeltas_API},
59 identifiers::{
60 AccountId, ClientOrderId, InstrumentId, StrategyId, Symbol, TraderId, VenueOrderId,
61 },
62 instruments::{Instrument, InstrumentAny},
63 python::{data::data_to_pycapsule, instruments::pyobject_to_instrument_any},
64 reports::{FillReport, OrderStatusReport},
65};
66use pyo3::{IntoPyObjectExt, prelude::*};
67use tokio_util::sync::CancellationToken;
68use ustr::Ustr;
69
70use crate::{
71 common::{
72 consts::KRAKEN_VENUE,
73 enums::{KrakenEnvironment, KrakenProductType},
74 urls::get_kraken_ws_private_url,
75 },
76 config::KrakenDataClientConfig,
77 websocket::spot_v2::{
78 client::KrakenSpotWebSocketClient,
79 level_2::L2BookState,
80 level_3::{
81 BookOrderIdHasher, KrakenL3WsMessage,
82 resync::retry_l3_resync,
83 runtime::{L3Sink, L3State, process_l3_message},
84 },
85 messages::KrakenSpotWsMessage,
86 parse::{
87 parse_quote_tick, parse_trade_tick, parse_ws_bar, parse_ws_fill_report,
88 parse_ws_order_status_report,
89 },
90 },
91};
92
93#[pymethods]
94#[pyo3_stub_gen::derive::gen_stub_pymethods]
95impl KrakenSpotWebSocketClient {
96 #[new]
98 #[pyo3(signature = (
99 environment=None,
100 private=false,
101 base_url=None,
102 heartbeat_secs=None,
103 api_key=None,
104 api_secret=None,
105 proxy_url=None,
106 l3=false,
107 validate_l3_checksum=true,
108 base_url_http=None,
109 ))]
110 #[expect(clippy::too_many_arguments)]
111 fn py_new(
112 environment: Option<KrakenEnvironment>,
113 private: bool,
114 base_url: Option<String>,
115 heartbeat_secs: Option<u64>,
116 api_key: Option<String>,
117 api_secret: Option<String>,
118 proxy_url: Option<String>,
119 l3: bool,
120 validate_l3_checksum: bool,
121 base_url_http: Option<String>,
122 ) -> PyResult<Self> {
123 if l3 && private {
124 return Err(to_pyvalue_err("`l3` and `private` are mutually exclusive"));
125 }
126
127 let env = environment.unwrap_or(KrakenEnvironment::Live);
128
129 let (resolved_api_key, resolved_api_secret) =
130 crate::common::credential::KrakenCredential::resolve_spot(api_key, api_secret)
131 .map(|c| c.into_parts())
132 .map_or((None, None), |(k, s)| (Some(k), Some(s)));
133
134 let (ws_public_url, ws_private_url, ws_l3_url) = if l3 {
135 (None, None, base_url)
136 } else if private {
137 let private_url = base_url.unwrap_or_else(|| {
138 get_kraken_ws_private_url(KrakenProductType::Spot, env).to_string()
139 });
140 (None, Some(private_url), None)
141 } else {
142 (base_url, None, None)
143 };
144
145 let config = KrakenDataClientConfig {
146 environment: env,
147 base_url: base_url_http,
148 ws_public_url,
149 ws_private_url,
150 ws_l3_url,
151 heartbeat_interval_secs: heartbeat_secs
152 .unwrap_or(KrakenDataClientConfig::default().heartbeat_interval_secs),
153 api_key: resolved_api_key,
154 api_secret: resolved_api_secret,
155 proxy_url: proxy_url.clone(),
156 validate_l3_checksum,
157 ..Default::default()
158 };
159
160 let token = CancellationToken::new();
161 Ok(if l3 {
162 Self::l3(config, token, proxy_url)
163 } else {
164 Self::new(config, token, proxy_url)
165 })
166 }
167
168 #[getter]
170 #[pyo3(name = "url")]
171 #[must_use]
172 pub fn py_url(&self) -> &str {
173 self.url()
174 }
175
176 #[getter]
179 #[pyo3(name = "has_credentials")]
180 fn py_has_credentials(&self) -> bool {
181 self.has_credentials()
182 }
183
184 #[pyo3(name = "is_connected")]
186 fn py_is_connected(&self) -> bool {
187 self.is_connected()
188 }
189
190 #[pyo3(name = "is_active")]
192 fn py_is_active(&self) -> bool {
193 self.is_active()
194 }
195
196 #[pyo3(name = "is_closed")]
198 fn py_is_closed(&self) -> bool {
199 self.is_closed()
200 }
201
202 #[pyo3(name = "get_subscriptions")]
204 fn py_get_subscriptions(&self) -> Vec<String> {
205 self.get_subscriptions()
206 }
207
208 #[pyo3(name = "cancel_all_requests")]
210 fn py_cancel_all_requests(&self) {
211 self.cancel_all_requests();
212 }
213
214 #[pyo3(name = "connect")]
216 #[expect(clippy::needless_pass_by_value)]
217 fn py_connect<'py>(
218 &mut self,
219 py: Python<'py>,
220 loop_: Py<PyAny>,
221 instruments: Vec<Py<PyAny>>,
222 callback: Py<PyAny>,
223 ) -> PyResult<Bound<'py, PyAny>> {
224 let call_soon: Py<PyAny> = loop_.getattr(py, "call_soon_threadsafe")?;
225
226 let instruments_map = Arc::new(AtomicMap::<InstrumentId, InstrumentAny>::new());
227
228 for inst in instruments {
229 let inst_any = pyobject_to_instrument_any(py, inst)?;
230 instruments_map.insert(inst_any.id(), inst_any.clone());
231 self.cache_instrument(inst_any);
232 }
233
234 let account_id = self.account_id_shared().clone();
235 let truncated_id_map = self.truncated_id_map().clone();
236 let mut client = self.clone();
237
238 pyo3_async_runtimes::tokio::future_into_py(py, async move {
239 client.connect().await.map_err(to_pyruntime_err)?;
240
241 let stream = client.stream().map_err(to_pyruntime_err)?;
242 let clock = get_atomic_clock_realtime();
243 let book_sequence = Arc::new(AtomicU64::new(0));
244
245 get_runtime().spawn(async move {
246 tokio::pin!(stream);
247 let order_qty_cache: Arc<AtomicMap<String, f64>> =
248 Arc::new(AtomicMap::new());
249 let order_instrument_cache: Arc<AtomicMap<String, InstrumentAny>> =
250 Arc::new(AtomicMap::new());
251
252 let mut l3_states: AHashMap<String, L3State> = AHashMap::new();
253 let l3_hasher = BookOrderIdHasher::new();
254 let l3_depths = client.l3_depths_handle();
255 let l2_depths = client.l2_depths_handle();
256 let l3_instruments = client.instruments_handle();
257 let l3_validate = client.validate_l3_checksum();
258 let client_for_l3_resync = client.clone();
259 let mut l2_books = L2BookState::default();
260
261 while let Some(msg) = stream.next().await {
262 let ts_init = clock.get_time_ns();
263
264 match msg {
265 KrakenSpotWsMessage::Ticker(tickers) => {
266 let instruments = instruments_map.load();
267
268 for ticker in &tickers {
269 let instrument_id = InstrumentId::new(
270 Symbol::new(ticker.symbol.as_str()),
271 *KRAKEN_VENUE,
272 );
273 let instrument = instruments.get(&instrument_id);
274
275 if let Some(inst) = instrument {
276 match parse_quote_tick(ticker, inst, ts_init) {
277 Ok(quote) => {
278 Python::attach(|py| {
279 let py_obj =
280 data_to_pycapsule(py, Data::Quote(quote));
281 call_python_threadsafe(
282 py, &call_soon, &callback, py_obj,
283 );
284 });
285 }
286 Err(e) => {
287 log::error!("Failed to parse quote tick: {e}");
288 }
289 }
290 }
291 }
292 }
293 KrakenSpotWsMessage::Trade(trades) => {
294 let instruments = instruments_map.load();
295
296 for trade in &trades {
297 let instrument_id = InstrumentId::new(
298 Symbol::new(trade.symbol.as_str()),
299 *KRAKEN_VENUE,
300 );
301 let instrument = instruments.get(&instrument_id);
302
303 if let Some(inst) = instrument {
304 match parse_trade_tick(trade, inst, ts_init) {
305 Ok(tick) => {
306 Python::attach(|py| {
307 let py_obj =
308 data_to_pycapsule(py, Data::Trade(tick));
309 call_python_threadsafe(
310 py, &call_soon, &callback, py_obj,
311 );
312 });
313 }
314 Err(e) => {
315 log::error!("Failed to parse trade tick: {e}");
316 }
317 }
318 }
319 }
320 }
321 KrakenSpotWsMessage::Book {
322 data,
323 is_snapshot,
324 } => {
325 let instruments = instruments_map.load();
326
327 for book in &data {
328 let instrument_id = InstrumentId::new(
329 Symbol::new(book.symbol.as_str()),
330 *KRAKEN_VENUE,
331 );
332 let instrument = instruments.get(&instrument_id);
333
334 if let Some(inst) = instrument {
335 let sequence = book_sequence.load(Ordering::Relaxed);
336 let depth = l2_depths.get(book.symbol.as_str());
337 match l2_books.process_book(
338 book,
339 inst,
340 sequence,
341 is_snapshot,
342 depth,
343 ts_init,
344 ) {
345 Ok(Some((deltas, next_sequence))) => {
346 book_sequence.store(next_sequence, Ordering::Relaxed);
347 Python::attach(|py| {
348 let py_obj = data_to_pycapsule(
349 py,
350 Data::Deltas(OrderBookDeltas_API::new(deltas)),
351 );
352 call_python_threadsafe(
353 py, &call_soon, &callback, py_obj,
354 );
355 });
356 }
357 Ok(None) => {}
358 Err(e) => {
359 log::error!("Failed to parse book deltas: {e}");
360 }
361 }
362 }
363 }
364 }
365 KrakenSpotWsMessage::Ohlc(ohlc_data) => {
366 let instruments = instruments_map.load();
367
368 for ohlc in &ohlc_data {
369 let instrument_id = InstrumentId::new(
370 Symbol::new(ohlc.symbol.as_str()),
371 *KRAKEN_VENUE,
372 );
373 let instrument = instruments.get(&instrument_id);
374
375 if let Some(inst) = instrument {
376 match parse_ws_bar(ohlc, inst, ts_init) {
377 Ok(bar) => {
378 Python::attach(|py| {
379 let py_obj = data_to_pycapsule(py, Data::Bar(bar));
380 call_python_threadsafe(
381 py, &call_soon, &callback, py_obj,
382 );
383 });
384 }
385 Err(e) => {
386 log::error!("Failed to parse bar: {e}");
387 }
388 }
389 }
390 }
391 }
392 KrakenSpotWsMessage::Execution(executions) => {
393 let acct_id = account_id.read().ok().and_then(|g| *g);
394 let Some(acct_id) = acct_id else {
395 log::trace!(
396 "Execution message received but no account_id set (data-only client)"
397 );
398 continue;
399 };
400
401 let instruments = instruments_map.load();
402
403 for exec in &executions {
404 let inst = if let Some(ref symbol) = exec.symbol {
405 let instrument_id = InstrumentId::new(
406 Symbol::new(symbol.as_str()),
407 *KRAKEN_VENUE,
408 );
409 let Some(inst) = instruments.get(&instrument_id).cloned()
410 else {
411 log::warn!("No instrument for symbol: {symbol}");
412 continue;
413 };
414
415 if let Some(ref id) = exec.cl_ord_id {
416 order_instrument_cache.insert(id.clone(), inst.clone());
417 }
418 order_instrument_cache
419 .insert(exec.order_id.clone(), inst.clone());
420 inst
421 } else {
422 let cache = order_instrument_cache.load();
423 let found = exec
424 .cl_ord_id
425 .as_ref()
426 .and_then(|id| cache.get(id).cloned())
427 .or_else(|| cache.get(&exec.order_id).cloned());
428 let Some(inst) = found else {
429 log::debug!(
430 "Execution without symbol and no cached instrument: \
431 exec_type={:?}, order_id={}",
432 exec.exec_type,
433 exec.order_id
434 );
435 continue;
436 };
437 inst
438 };
439
440 let cached_qty = exec.cl_ord_id.as_ref().and_then(|id| {
441 order_qty_cache.load().get(id).copied()
442 });
443
444 if let (Some(qty), Some(cl_ord_id)) =
445 (exec.order_qty, &exec.cl_ord_id)
446 {
447 order_qty_cache.insert(cl_ord_id.clone(), qty);
448 }
449
450 match parse_ws_order_status_report(
451 exec, &inst, acct_id, cached_qty, ts_init,
452 ) {
453 Ok(mut report) => {
454 if let Some(ref cl_ord_id) = exec.cl_ord_id {
455 let full_id = truncated_id_map
456 .load()
457 .get(cl_ord_id)
458 .copied()
459 .unwrap_or_else(|| ClientOrderId::new(cl_ord_id));
460 report = report.with_client_order_id(full_id);
461 }
462 dispatch_order_status_report(
463 report, &call_soon, &callback,
464 );
465 }
466 Err(e) => {
467 log::error!("Failed to parse order status report: {e}");
468 }
469 }
470
471 if exec.exec_id.is_some() {
472 match parse_ws_fill_report(exec, &inst, acct_id, ts_init) {
473 Ok(mut report) => {
474 if let Some(ref cl_ord_id) = exec.cl_ord_id {
475 let full_id = truncated_id_map
476 .load()
477 .get(cl_ord_id)
478 .copied()
479 .unwrap_or_else(|| {
480 ClientOrderId::new(cl_ord_id)
481 });
482 report.client_order_id = Some(full_id);
483 }
484 dispatch_fill_report(report, &call_soon, &callback);
485 }
486 Err(e) => {
487 log::error!("Failed to parse fill report: {e}");
488 }
489 }
490 }
491 }
492 }
493 KrakenSpotWsMessage::L3Snapshot(snap) => {
494 let mut sink = PyDeltaSink {
495 call_soon: &call_soon,
496 callback: &callback,
497 };
498 run_l3_state(
499 KrakenL3WsMessage::Snapshot(snap),
500 &mut sink,
501 &l3_instruments,
502 &l3_depths,
503 &mut l3_states,
504 &l3_hasher,
505 l3_validate,
506 ts_init,
507 &client_for_l3_resync,
508 )
509 .await;
510 }
511 KrakenSpotWsMessage::L3Update(update) => {
512 let mut sink = PyDeltaSink {
513 call_soon: &call_soon,
514 callback: &callback,
515 };
516 run_l3_state(
517 KrakenL3WsMessage::Update(update),
518 &mut sink,
519 &l3_instruments,
520 &l3_depths,
521 &mut l3_states,
522 &l3_hasher,
523 l3_validate,
524 ts_init,
525 &client_for_l3_resync,
526 )
527 .await;
528 }
529 KrakenSpotWsMessage::Reconnected => {
530 log::info!("WebSocket reconnected");
531
532 for state in l3_states.values_mut() {
533 state.open_orders.clear();
534 state.awaiting_snapshot = true;
535 }
536 }
537 KrakenSpotWsMessage::OrderResponse(_) => {}
538 }
539 }
540 });
541
542 Ok(())
543 })
544 }
545
546 #[pyo3(name = "wait_until_active")]
548 fn py_wait_until_active<'py>(
549 &self,
550 py: Python<'py>,
551 timeout_secs: f64,
552 ) -> PyResult<Bound<'py, PyAny>> {
553 let client = self.clone();
554
555 pyo3_async_runtimes::tokio::future_into_py(py, async move {
556 client
557 .wait_until_active(timeout_secs)
558 .await
559 .map_err(to_pyruntime_err)?;
560 Ok(())
561 })
562 }
563
564 #[pyo3(name = "authenticate")]
566 fn py_authenticate<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
567 let client = self.clone();
568
569 pyo3_async_runtimes::tokio::future_into_py(py, async move {
570 client.authenticate().await.map_err(to_pyruntime_err)?;
571 Ok(())
572 })
573 }
574
575 #[pyo3(name = "is_authenticated")]
577 fn py_is_authenticated(&self) -> bool {
578 self.is_authenticated()
579 }
580
581 #[pyo3(name = "wait_until_authenticated")]
585 fn py_wait_until_authenticated<'py>(
586 &self,
587 py: Python<'py>,
588 timeout_secs: f64,
589 ) -> PyResult<Bound<'py, PyAny>> {
590 let client = self.clone();
591
592 pyo3_async_runtimes::tokio::future_into_py(py, async move {
593 client
594 .wait_until_authenticated(timeout_secs)
595 .await
596 .map_err(to_pyruntime_err)?;
597 Ok(())
598 })
599 }
600
601 #[pyo3(name = "disconnect")]
603 fn py_disconnect<'py>(&mut self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
604 let mut client = self.clone();
605
606 pyo3_async_runtimes::tokio::future_into_py(py, async move {
607 client.disconnect().await.map_err(to_pyruntime_err)?;
608 Ok(())
609 })
610 }
611
612 #[pyo3(name = "send_ping")]
614 fn py_send_ping<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
615 let client = self.clone();
616
617 pyo3_async_runtimes::tokio::future_into_py(py, async move {
618 client.send_ping().await.map_err(to_pyruntime_err)?;
619 Ok(())
620 })
621 }
622
623 #[pyo3(name = "close")]
625 fn py_close<'py>(&mut self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
626 let mut client = self.clone();
627
628 pyo3_async_runtimes::tokio::future_into_py(py, async move {
629 client.close().await.map_err(to_pyruntime_err)?;
630 Ok(())
631 })
632 }
633
634 #[pyo3(name = "set_account_id")]
636 fn py_set_account_id(&self, account_id: AccountId) {
637 self.set_account_id(account_id);
638 }
639
640 #[pyo3(name = "cache_instrument")]
642 fn py_cache_instrument(&self, py: Python, instrument: Py<PyAny>) -> PyResult<()> {
643 let inst_any = pyobject_to_instrument_any(py, instrument)?;
644 self.cache_instrument(inst_any);
645 Ok(())
646 }
647
648 #[pyo3(name = "cache_client_order")]
650 fn py_cache_client_order(
651 &self,
652 client_order_id: ClientOrderId,
653 venue_order_id: Option<VenueOrderId>,
654 instrument_id: InstrumentId,
655 trader_id: TraderId,
656 strategy_id: StrategyId,
657 ) {
658 self.cache_client_order(
659 client_order_id,
660 venue_order_id,
661 instrument_id,
662 trader_id,
663 strategy_id,
664 );
665 }
666
667 #[pyo3(name = "subscribe_book")]
669 fn py_subscribe_book<'py>(
670 &self,
671 py: Python<'py>,
672 instrument_id: InstrumentId,
673 depth: Option<u32>,
674 ) -> PyResult<Bound<'py, PyAny>> {
675 let client = self.clone();
676
677 pyo3_async_runtimes::tokio::future_into_py(py, async move {
678 client
679 .subscribe_book(instrument_id, depth)
680 .await
681 .map_err(to_pyruntime_err)?;
682 Ok(())
683 })
684 }
685
686 #[pyo3(name = "subscribe_l3_book")]
695 #[expect(clippy::needless_pass_by_value)]
696 fn py_subscribe_l3_book<'py>(
697 &self,
698 py: Python<'py>,
699 symbol: String,
700 depth: u32,
701 ) -> PyResult<Bound<'py, PyAny>> {
702 let client = self.clone();
703 let symbol = Ustr::from(&symbol);
704 pyo3_async_runtimes::tokio::future_into_py(py, async move {
705 client
706 .subscribe_book_l3(symbol, depth)
707 .await
708 .map_err(to_pyruntime_err)
709 })
710 }
711
712 #[pyo3(name = "unsubscribe_l3_book")]
714 #[expect(clippy::needless_pass_by_value)]
715 fn py_unsubscribe_l3_book<'py>(
716 &self,
717 py: Python<'py>,
718 symbol: String,
719 ) -> PyResult<Bound<'py, PyAny>> {
720 let client = self.clone();
721 let symbol = Ustr::from(&symbol);
722 pyo3_async_runtimes::tokio::future_into_py(py, async move {
723 client
724 .unsubscribe_book_l3(symbol)
725 .await
726 .map_err(to_pyruntime_err)
727 })
728 }
729
730 #[pyo3(name = "subscribe_quotes")]
735 fn py_subscribe_quotes<'py>(
736 &self,
737 py: Python<'py>,
738 instrument_id: InstrumentId,
739 ) -> PyResult<Bound<'py, PyAny>> {
740 let client = self.clone();
741
742 pyo3_async_runtimes::tokio::future_into_py(py, async move {
743 client
744 .subscribe_quotes(instrument_id)
745 .await
746 .map_err(to_pyruntime_err)?;
747 Ok(())
748 })
749 }
750
751 #[pyo3(name = "subscribe_trades")]
753 fn py_subscribe_trades<'py>(
754 &self,
755 py: Python<'py>,
756 instrument_id: InstrumentId,
757 ) -> PyResult<Bound<'py, PyAny>> {
758 let client = self.clone();
759
760 pyo3_async_runtimes::tokio::future_into_py(py, async move {
761 client
762 .subscribe_trades(instrument_id)
763 .await
764 .map_err(to_pyruntime_err)?;
765 Ok(())
766 })
767 }
768
769 #[pyo3(name = "subscribe_bars")]
771 fn py_subscribe_bars<'py>(
772 &self,
773 py: Python<'py>,
774 bar_type: BarType,
775 ) -> PyResult<Bound<'py, PyAny>> {
776 let client = self.clone();
777
778 pyo3_async_runtimes::tokio::future_into_py(py, async move {
779 client
780 .subscribe_bars(bar_type)
781 .await
782 .map_err(to_pyruntime_err)?;
783 Ok(())
784 })
785 }
786
787 #[pyo3(name = "subscribe_executions")]
791 #[pyo3(signature = (snap_orders=true, snap_trades=true))]
792 fn py_subscribe_executions<'py>(
793 &self,
794 py: Python<'py>,
795 snap_orders: bool,
796 snap_trades: bool,
797 ) -> PyResult<Bound<'py, PyAny>> {
798 let client = self.clone();
799
800 pyo3_async_runtimes::tokio::future_into_py(py, async move {
801 client
802 .subscribe_executions(snap_orders, snap_trades)
803 .await
804 .map_err(to_pyruntime_err)?;
805 Ok(())
806 })
807 }
808
809 #[pyo3(name = "unsubscribe_book")]
811 fn py_unsubscribe_book<'py>(
812 &self,
813 py: Python<'py>,
814 instrument_id: InstrumentId,
815 ) -> PyResult<Bound<'py, PyAny>> {
816 let client = self.clone();
817
818 pyo3_async_runtimes::tokio::future_into_py(py, async move {
819 client
820 .unsubscribe_book(instrument_id)
821 .await
822 .map_err(to_pyruntime_err)?;
823 Ok(())
824 })
825 }
826
827 #[pyo3(name = "unsubscribe_quotes")]
829 fn py_unsubscribe_quotes<'py>(
830 &self,
831 py: Python<'py>,
832 instrument_id: InstrumentId,
833 ) -> PyResult<Bound<'py, PyAny>> {
834 let client = self.clone();
835
836 pyo3_async_runtimes::tokio::future_into_py(py, async move {
837 client
838 .unsubscribe_quotes(instrument_id)
839 .await
840 .map_err(to_pyruntime_err)?;
841 Ok(())
842 })
843 }
844
845 #[pyo3(name = "unsubscribe_trades")]
847 fn py_unsubscribe_trades<'py>(
848 &self,
849 py: Python<'py>,
850 instrument_id: InstrumentId,
851 ) -> PyResult<Bound<'py, PyAny>> {
852 let client = self.clone();
853
854 pyo3_async_runtimes::tokio::future_into_py(py, async move {
855 client
856 .unsubscribe_trades(instrument_id)
857 .await
858 .map_err(to_pyruntime_err)?;
859 Ok(())
860 })
861 }
862
863 #[pyo3(name = "unsubscribe_bars")]
865 fn py_unsubscribe_bars<'py>(
866 &self,
867 py: Python<'py>,
868 bar_type: BarType,
869 ) -> PyResult<Bound<'py, PyAny>> {
870 let client = self.clone();
871
872 pyo3_async_runtimes::tokio::future_into_py(py, async move {
873 client
874 .unsubscribe_bars(bar_type)
875 .await
876 .map_err(to_pyruntime_err)?;
877 Ok(())
878 })
879 }
880}
881
882fn dispatch_order_status_report(
883 report: OrderStatusReport,
884 call_soon: &Py<PyAny>,
885 callback: &Py<PyAny>,
886) {
887 Python::attach(|py| match report.into_py_any(py) {
888 Ok(py_obj) => {
889 call_python_threadsafe(py, call_soon, callback, py_obj);
890 }
891 Err(e) => {
892 log::error!("Failed to convert OrderStatusReport to Python: {e}");
893 }
894 });
895}
896
897fn dispatch_fill_report(report: FillReport, call_soon: &Py<PyAny>, callback: &Py<PyAny>) {
898 Python::attach(|py| match report.into_py_any(py) {
899 Ok(py_obj) => {
900 call_python_threadsafe(py, call_soon, callback, py_obj);
901 }
902 Err(e) => {
903 log::error!("Failed to convert FillReport to Python: {e}");
904 }
905 });
906}
907
908struct PyDeltaSink<'a> {
909 call_soon: &'a Py<PyAny>,
910 callback: &'a Py<PyAny>,
911}
912
913impl L3Sink for PyDeltaSink<'_> {
914 fn emit_deltas(&mut self, deltas: OrderBookDeltas_API) {
915 Python::attach(|py| {
916 let py_obj = data_to_pycapsule(py, Data::Deltas(deltas));
917 call_python_threadsafe(py, self.call_soon, self.callback, py_obj);
918 });
919 }
920}
921
922#[expect(clippy::too_many_arguments)]
923async fn run_l3_state(
924 msg: KrakenL3WsMessage,
925 sink: &mut PyDeltaSink<'_>,
926 instruments: &Arc<AtomicMap<InstrumentId, InstrumentAny>>,
927 depths: &Arc<std::sync::Mutex<AHashMap<String, u32>>>,
928 states: &mut AHashMap<String, L3State>,
929 hasher: &BookOrderIdHasher,
930 validate_checksum: bool,
931 ts_init: UnixNanos,
932 client: &KrakenSpotWebSocketClient,
933) {
934 let resync = process_l3_message(
935 msg,
936 sink,
937 instruments,
938 depths,
939 states,
940 hasher,
941 validate_checksum,
942 ts_init,
943 );
944
945 if let Some(request) = resync {
946 log::warn!(
947 "Resyncing Kraken L3 book: symbol={}, depth={}, reason={}",
948 request.symbol,
949 request.depth,
950 request.reason,
951 );
952 let symbol_ustr = Ustr::from(&request.symbol);
953 let client = client.clone();
954 get_runtime().spawn(async move {
955 retry_l3_resync(&client, symbol_ustr, request.depth).await;
956 });
957 }
958}