1mod auto_load;
19mod dispatch;
20mod effective_deltas;
21mod instruments;
22mod lifecycle;
23mod requests;
24mod runtime;
25mod subscriptions;
26
27use std::{
28 future::Future,
29 sync::{
30 Arc,
31 atomic::{AtomicBool, Ordering},
32 },
33 time::Duration,
34};
35
36use dashmap::DashMap;
37use nautilus_common::{
38 cache::InstrumentLookupError,
39 clients::DataClient,
40 live::{dst::time::Instant, runner::get_data_event_sender, sender::EventSender},
41 messages::{
42 DataEvent,
43 data::{
44 RequestBookSnapshot, RequestCustomData, RequestInstrument, RequestInstruments,
45 RequestTrades, SubscribeBookDeltas, SubscribeBookDepth, SubscribeCustomData,
46 SubscribeInstrument, SubscribeInstrumentClose, SubscribeInstrumentStatus,
47 SubscribeInstruments, SubscribeQuotes, SubscribeTrades, UnsubscribeBookDeltas,
48 UnsubscribeCustomData, UnsubscribeInstrument, UnsubscribeInstrumentClose,
49 UnsubscribeInstrumentStatus, UnsubscribeQuotes, UnsubscribeTrades,
50 },
51 },
52 msgbus::TypedHandler,
53};
54use nautilus_core::{
55 AtomicMap,
56 time::{AtomicTime, get_atomic_clock_realtime},
57};
58use nautilus_live::{
59 SocketControl, SocketControlFactory,
60 task::{TaskGroup, TaskSpawner},
61};
62use nautilus_model::{
63 enums::BookType,
64 events::PositionEvent,
65 identifiers::{ClientId, InstrumentId, Venue},
66 instruments::InstrumentAny,
67};
68use nautilus_network::websocket::proxy::ProxyUrl;
69use parking_lot::Mutex;
70use tokio_util::sync::CancellationToken;
71use ustr::Ustr;
72
73pub(crate) use self::subscriptions::DataSubscriptions;
74use self::{
75 instruments::InstrumentUpdateState,
76 requests::{
77 request_book_snapshot, request_data, request_instrument, request_instruments,
78 request_trades,
79 },
80 runtime::is_instrument_expired_and_not_reported_open,
81 subscriptions::{SubscriptionKind, resolve_token_id_from},
82};
83use crate::{
84 book::{BookSequenceOutcome, recovery::start_recovery},
85 common::{consts::POLYMARKET_VENUE, credential::Credential},
86 config::PolymarketDataClientConfig,
87 filters::InstrumentFilter,
88 http::{
89 clob::PolymarketClobPublicClient, data_api::PolymarketDataApiHttpClient,
90 gamma::PolymarketGammaHttpClient,
91 },
92 polybolt::{PolymarketPolyBoltFeed, is_supported_polybolt_data_type},
93 providers::PolymarketInstrumentProvider,
94 resolve::{PendingResolution, ResolveContext, ResolveWatchEntry, StrictResolvedMarket},
95 websocket::{POLYBOLT_STREAMS_ENDPOINT, pool::PolymarketMarketConnectionPool},
96};
97
98const NEW_MARKET_FETCH_MAX_CONCURRENCY_CAP: usize = 64;
99pub(super) const NEW_MARKET_EMPTY_RECHECK_MAX_ATTEMPTS: usize = 1;
100pub(super) const NEW_MARKET_EMPTY_RECHECK_DELAY: Duration = Duration::from_millis(500);
101
102#[derive(Debug)]
110pub struct PolymarketDataClient {
111 clock: &'static AtomicTime,
112 client_id: ClientId,
113 config: PolymarketDataClientConfig,
114 provider: PolymarketInstrumentProvider,
115 clob_public_client: PolymarketClobPublicClient,
116 data_api_client: PolymarketDataApiHttpClient,
117 ws_client: PolymarketMarketConnectionPool,
118 is_connected: AtomicBool,
119 cancellation_token: CancellationToken,
120 tasks: TaskGroup,
121 data_sender: EventSender<DataEvent>,
122 instruments: Arc<AtomicMap<InstrumentId, InstrumentAny>>,
123 subscriptions: DataSubscriptions,
124 instrument_update_state: Arc<Mutex<InstrumentUpdateState>>,
125 resolve_poll_watchlist: Arc<AtomicMap<String, ResolveWatchEntry>>,
126 resolve_watch_apply_mutex: Arc<Mutex<()>>,
127 pending_resolutions: Arc<DashMap<String, PendingResolution>>,
128 deferred_resolutions: Arc<AtomicMap<InstrumentId, StrictResolvedMarket>>,
129 new_market_inflight_keys: Arc<DashMap<String, ()>>,
130 new_market_fetch_semaphore: Arc<tokio::sync::Semaphore>,
131 auto_load_scheduled: Arc<AtomicBool>,
132 position_event_handler: Option<TypedHandler<PositionEvent>>,
133 polybolt_feed: PolymarketPolyBoltFeed,
134 polybolt_socket_control: Option<SocketControl>,
135 polybolt_credential: Option<Credential>,
136 proxy_url: Option<ProxyUrl>,
137 reset_pending: bool,
138 shutdown_errors: Vec<String>,
139}
140
141impl PolymarketDataClient {
142 pub fn new(
144 client_id: ClientId,
145 config: PolymarketDataClientConfig,
146 gamma_client: PolymarketGammaHttpClient,
147 clob_public_client: PolymarketClobPublicClient,
148 data_api_client: PolymarketDataApiHttpClient,
149 ws_client: PolymarketMarketConnectionPool,
150 ) -> Self {
151 Self::new_with_proxy(
152 client_id,
153 config,
154 gamma_client,
155 clob_public_client,
156 data_api_client,
157 ws_client,
158 None,
159 )
160 }
161
162 pub fn new_with_proxy(
164 client_id: ClientId,
165 mut config: PolymarketDataClientConfig,
166 mut gamma_client: PolymarketGammaHttpClient,
167 clob_public_client: PolymarketClobPublicClient,
168 data_api_client: PolymarketDataApiHttpClient,
169 ws_client: PolymarketMarketConnectionPool,
170 proxy_url: Option<ProxyUrl>,
171 ) -> Self {
172 let clock = get_atomic_clock_realtime();
173 let data_sender = get_data_event_sender();
174 let socket_factory = SocketControlFactory::new(client_id, Some(*POLYMARKET_VENUE));
175 let ws_client = ws_client.with_socket_factory(socket_factory.clone());
176 let polybolt_socket_control = Some(socket_factory.control(POLYBOLT_STREAMS_ENDPOINT));
177 gamma_client.set_clob_client(clob_public_client.clone());
178 let provider =
179 PolymarketInstrumentProvider::new(gamma_client, config.instrument_config.clone());
180 let configured_fetch_max_concurrency = config.new_market_fetch_max_concurrency;
181 let fetch_max_concurrency =
182 clamp_new_market_fetch_max_concurrency(configured_fetch_max_concurrency);
183
184 if configured_fetch_max_concurrency == 0 {
185 log::warn!(
186 "PolymarketDataClientConfig.new_market_fetch_max_concurrency=0 is invalid, clamping to 1"
187 );
188 } else if configured_fetch_max_concurrency > NEW_MARKET_FETCH_MAX_CONCURRENCY_CAP {
189 log::warn!(
190 "PolymarketDataClientConfig.new_market_fetch_max_concurrency={configured_fetch_max_concurrency} exceeds cap {NEW_MARKET_FETCH_MAX_CONCURRENCY_CAP}, clamping",
191 );
192 }
193 config.new_market_fetch_max_concurrency = fetch_max_concurrency;
194
195 let polybolt_url = config.reference_prices_url();
196 let polybolt_transport_backend = config.transport_backend;
197 let polybolt_data_sender = data_sender.clone();
198
199 let polybolt_credential = config.polybolt_credential().unwrap_or_else(|e| {
200 log::error!("{e}; PolyBolt price subscriptions will be rejected");
201 None
202 });
203
204 let tasks = TaskGroup::new();
205 let cancellation_token = tasks.cancellation_token();
206
207 let instruments = Arc::new(AtomicMap::new());
208 let resolve_poll_watchlist = Arc::new(AtomicMap::new());
209 let resolve_watch_apply_mutex = Arc::new(Mutex::new(()));
210 let deferred_resolutions = Arc::new(AtomicMap::new());
211
212 let subscriptions = DataSubscriptions::new(
213 instruments.clone(),
214 resolve_poll_watchlist.clone(),
215 deferred_resolutions.clone(),
216 resolve_watch_apply_mutex.clone(),
217 ws_client.handle(),
218 config.subscribe_new_markets,
219 );
220
221 Self {
222 clock,
223 client_id,
224 config,
225 provider,
226 clob_public_client,
227 data_api_client,
228 ws_client,
229 is_connected: AtomicBool::new(false),
230 cancellation_token,
231 tasks,
232 data_sender,
233 instruments,
234 subscriptions,
235 instrument_update_state: Arc::new(Mutex::new(InstrumentUpdateState::default())),
236 resolve_poll_watchlist,
237 resolve_watch_apply_mutex,
238 pending_resolutions: Arc::new(DashMap::new()),
239 deferred_resolutions,
240 new_market_inflight_keys: Arc::new(DashMap::new()),
241 new_market_fetch_semaphore: Arc::new(tokio::sync::Semaphore::new(
242 fetch_max_concurrency,
243 )),
244 auto_load_scheduled: Arc::new(AtomicBool::new(false)),
245 position_event_handler: None,
246 polybolt_feed: PolymarketPolyBoltFeed::new_with_proxy_and_socket_control(
247 polybolt_url,
248 polybolt_transport_backend,
249 clock,
250 polybolt_data_sender,
251 proxy_url.clone(),
252 polybolt_socket_control.clone(),
253 polybolt_credential.clone(),
254 ),
255 polybolt_socket_control,
256 polybolt_credential,
257 proxy_url,
258 reset_pending: false,
259 shutdown_errors: Vec::new(),
260 }
261 }
262
263 #[must_use]
265 pub fn config(&self) -> &PolymarketDataClientConfig {
266 &self.config
267 }
268
269 #[must_use]
271 pub fn venue(&self) -> Venue {
272 *POLYMARKET_VENUE
273 }
274
275 #[must_use]
277 pub fn provider(&self) -> &PolymarketInstrumentProvider {
278 &self.provider
279 }
280
281 #[cfg(test)]
282 pub(crate) fn clob_public_client(&self) -> &PolymarketClobPublicClient {
283 &self.clob_public_client
284 }
285
286 #[cfg(test)]
287 pub(crate) fn data_api_client(&self) -> &PolymarketDataApiHttpClient {
288 &self.data_api_client
289 }
290
291 #[cfg(test)]
292 pub(crate) fn ws_client(&self) -> &PolymarketMarketConnectionPool {
293 &self.ws_client
294 }
295
296 #[cfg(test)]
297 pub(crate) fn polybolt_feed(&self) -> &PolymarketPolyBoltFeed {
298 &self.polybolt_feed
299 }
300
301 pub fn add_instrument_filter(&mut self, filter: Arc<dyn InstrumentFilter>) {
303 self.provider.add_filter(filter);
304 }
305
306 #[must_use]
308 pub fn is_connected(&self) -> bool {
309 self.is_connected.load(Ordering::Relaxed)
310 }
311
312 fn resolve_token_id(&self, instrument_id: InstrumentId) -> anyhow::Result<String> {
313 resolve_token_id_from(&self.instruments, instrument_id)
314 }
315
316 fn resolution_context(&self) -> ResolveContext {
317 ResolveContext {
318 subscriptions: self.subscriptions.clone(),
319 clock: self.clock,
320 data_sender: self.data_sender.clone(),
321 instruments: self.instruments.clone(),
322 watchlist: self.resolve_poll_watchlist.clone(),
323 apply_mutex: self.resolve_watch_apply_mutex.clone(),
324 pending_resolutions: self.pending_resolutions.clone(),
325 deferred_resolutions: self.deferred_resolutions.clone(),
326 cancellation_token: self.cancellation_token.clone(),
327 }
328 }
329
330 fn ensure_live_subscription_allowed(&self, instrument_id: InstrumentId) -> anyhow::Result<()> {
331 let now_ns = self.clock.get_time_ns();
332 let loaded = self.instruments.load();
333 let Some(instrument) = loaded.get(&instrument_id) else {
334 return Ok(());
335 };
336
337 if is_instrument_expired_and_not_reported_open(instrument, now_ns) {
338 anyhow::bail!(
339 "Instrument {instrument_id} is expired and no longer available for live subscription"
340 );
341 }
342
343 Ok(())
344 }
345
346 fn ensure_market_data_request_allowed(
347 &self,
348 instrument_id: InstrumentId,
349 ) -> anyhow::Result<InstrumentAny> {
350 let loaded = self.instruments.load();
351 let instrument = loaded
352 .get(&instrument_id)
353 .ok_or_else(|| InstrumentLookupError::not_found(instrument_id))?
354 .clone();
355
356 if is_instrument_expired_and_not_reported_open(&instrument, self.clock.get_time_ns()) {
357 anyhow::bail!(
358 "Instrument {instrument_id} is expired and no longer available for market data requests"
359 );
360 }
361
362 Ok(instrument)
363 }
364
365 fn subscribe_resolution(
366 &self,
367 instrument_id: InstrumentId,
368 kind: SubscriptionKind,
369 ) -> anyhow::Result<()> {
370 let cached = self.instruments.load().contains_key(&instrument_id);
372
373 if !cached && !self.config.auto_load_missing_instruments {
374 anyhow::bail!(
375 "Instrument {instrument_id} not found, and `auto_load_missing_instruments` is disabled"
376 );
377 }
378
379 if !self
380 .subscriptions
381 .add_resolution_subscription_intent(instrument_id, kind)
382 {
383 return Ok(());
384 }
385
386 if !cached {
387 self.queue_pending_load(instrument_id);
388 return Ok(());
389 }
390
391 self.sync_ws_subscription(instrument_id);
392 Ok(())
393 }
394
395 fn sync_ws_subscription(&self, instrument_id: InstrumentId) {
400 if let Ok(token_id) = self.resolve_token_id(instrument_id) {
401 self.sync_ws_subscription_for_token(instrument_id, token_id);
402 }
403 }
404
405 fn sync_ws_subscription_for_token(&self, instrument_id: InstrumentId, token_id_str: String) {
406 let subscriptions = self.subscriptions.clone();
407 let future = async move {
408 subscriptions.reconcile(instrument_id, token_id_str).await;
409 };
410
411 if let Err(e) = self.tasks.spawn(future) {
412 log::debug!("Skipping Polymarket data task after shutdown began: {e}");
413 }
414 }
415
416 fn sync_book_subscription(&self, instrument_id: InstrumentId) {
419 let Ok(token_id) = self.resolve_token_id(instrument_id) else {
420 return;
421 };
422
423 let spawner = match self.tasks.spawner() {
424 Ok(spawner) => spawner,
425 Err(e) => {
426 log::debug!("Skipping Polymarket data task after shutdown began: {e}");
427 return;
428 }
429 };
430
431 let was_open = self
434 .subscriptions
435 .ws_open_tokens
436 .contains(&Ustr::from(token_id.as_str()));
437 let resolve_ctx = self.resolution_context();
438 let book_sync = self.subscriptions.book_sync.clone();
439 let snapshot_timeout = Duration::from_secs(self.config.book_snapshot_timeout_secs);
440
441 let future = async move {
443 resolve_ctx
444 .sync_ws_subscription(instrument_id, token_id.clone())
445 .await;
446
447 if was_open
448 && book_sync.request_snapshot_if_subscribed(
449 &resolve_ctx.subscriptions.active_delta_subs,
450 instrument_id,
451 Instant::now(),
452 ) == BookSequenceOutcome::Recover
453 {
454 log::debug!("Requesting a book snapshot for {instrument_id} on an open token");
455 start_recovery(
456 instrument_id,
457 Some(token_id),
458 &resolve_ctx.subscriptions.active_delta_subs,
459 &book_sync,
460 &resolve_ctx.subscriptions.ws_client,
461 snapshot_timeout,
462 &spawner,
463 );
464 }
465 };
466
467 if let Err(e) = self.tasks.spawn(future) {
468 log::debug!("Skipping Polymarket data task after shutdown began: {e}");
469 }
470 }
471}
472
473#[async_trait::async_trait(?Send)]
474impl DataClient for PolymarketDataClient {
475 fn client_id(&self) -> ClientId {
476 self.client_id
477 }
478
479 fn venue(&self) -> Option<Venue> {
480 Some(*POLYMARKET_VENUE)
481 }
482
483 fn start(&mut self) -> anyhow::Result<()> {
484 self.start_client();
485 Ok(())
486 }
487
488 fn stop(&mut self) -> anyhow::Result<()> {
489 self.stop_client();
490 Ok(())
491 }
492
493 fn reset(&mut self) -> anyhow::Result<()> {
494 self.reset_client();
495 Ok(())
496 }
497
498 fn dispose(&mut self) -> anyhow::Result<()> {
499 self.stop()
500 }
501
502 async fn connect(&mut self) -> anyhow::Result<()> {
503 self.connect_client().await
504 }
505
506 async fn disconnect(&mut self) -> anyhow::Result<()> {
507 self.disconnect_client().await
508 }
509
510 fn is_connected(&self) -> bool {
511 self.is_connected.load(Ordering::Relaxed)
512 }
513
514 fn is_disconnected(&self) -> bool {
515 !self.is_connected()
516 }
517
518 fn request_data(&self, request: RequestCustomData) -> anyhow::Result<()> {
519 request_data(self, request);
520 Ok(())
521 }
522
523 fn request_instruments(&self, request: RequestInstruments) -> anyhow::Result<()> {
524 request_instruments(self, request);
525 Ok(())
526 }
527
528 fn request_instrument(&self, request: RequestInstrument) -> anyhow::Result<()> {
529 request_instrument(self, request);
530 Ok(())
531 }
532
533 fn request_book_snapshot(&self, request: RequestBookSnapshot) -> anyhow::Result<()> {
534 request_book_snapshot(self, request)
535 }
536
537 fn request_trades(&self, request: RequestTrades) -> anyhow::Result<()> {
538 request_trades(self, request)
539 }
540
541 fn subscribe_instruments(&mut self, _cmd: SubscribeInstruments) -> anyhow::Result<()> {
542 log::debug!("subscribe_instruments: subscribed individually via data subscription methods");
543 Ok(())
544 }
545
546 fn subscribe_instrument(&mut self, cmd: SubscribeInstrument) -> anyhow::Result<()> {
547 log::debug!(
548 "Subscribed to instrument definition updates for {}; shared instrument sources remain active",
549 cmd.instrument_id
550 );
551
552 Ok(())
553 }
554
555 fn unsubscribe_instrument(&mut self, cmd: &UnsubscribeInstrument) -> anyhow::Result<()> {
556 log::debug!(
557 "Unsubscribed from instrument {}; shared instrument sources remain active",
558 cmd.instrument_id
559 );
560 Ok(())
561 }
562
563 fn subscribe(&mut self, cmd: SubscribeCustomData) -> anyhow::Result<()> {
564 if !is_supported_polybolt_data_type(&cmd.data_type) {
565 log::debug!(
566 "Ignoring unsupported Polymarket custom data subscription: {}",
567 cmd.data_type
568 );
569 return Ok(());
570 }
571
572 log::debug!(
573 "Tracking Polymarket PolyBolt custom data subscription: {}",
574 cmd.data_type
575 );
576 let changed = self.polybolt_feed.track_subscribe(cmd.data_type)?;
577 if !changed {
578 return Ok(());
579 }
580
581 if !self.is_connected() {
582 return Ok(());
583 }
584
585 self.polybolt_feed
586 .request_reconcile(crate::polybolt::ReconcileReason::DesiredChanged);
587
588 Ok(())
589 }
590
591 fn subscribe_book_deltas(&mut self, cmd: SubscribeBookDeltas) -> anyhow::Result<()> {
592 if cmd.book_type != BookType::L2_MBP {
593 anyhow::bail!(
594 "Polymarket only supports L2_MBP order book deltas, received {:?}",
595 cmd.book_type
596 );
597 }
598
599 let instrument_id = cmd.instrument_id;
600 self.ensure_live_subscription_allowed(instrument_id)?;
601 let cached = self.instruments.load().contains_key(&instrument_id);
602
603 if !cached && !self.config.auto_load_missing_instruments {
604 anyhow::bail!(
605 "Instrument {instrument_id} not found, and `auto_load_missing_instruments` is disabled"
606 );
607 }
608
609 if !self
611 .subscriptions
612 .add_delta_subscription_intent(instrument_id, self.config.compute_effective_deltas)
613 {
614 return Ok(());
615 }
616
617 if !cached {
618 self.queue_pending_load(instrument_id);
619 return Ok(());
620 }
621
622 self.sync_book_subscription(instrument_id);
623 Ok(())
624 }
625
626 fn subscribe_book_depth(&mut self, _cmd: SubscribeBookDepth) -> anyhow::Result<()> {
627 anyhow::bail!(
628 "Polymarket does not support OrderBookDepth subscriptions; use managed L2_MBP order book deltas"
629 )
630 }
631
632 fn subscribe_quotes(&mut self, cmd: SubscribeQuotes) -> anyhow::Result<()> {
633 let instrument_id = cmd.instrument_id;
634 self.ensure_live_subscription_allowed(instrument_id)?;
635 let cached = self.instruments.load().contains_key(&instrument_id);
636
637 if !cached && !self.config.auto_load_missing_instruments {
638 anyhow::bail!(
639 "Instrument {instrument_id} not found, and `auto_load_missing_instruments` is disabled"
640 );
641 }
642
643 if !self
644 .subscriptions
645 .add_live_subscription_intent(instrument_id, SubscriptionKind::Quotes)
646 {
647 return Ok(());
648 }
649
650 if !cached {
651 self.queue_pending_load(instrument_id);
652 return Ok(());
653 }
654
655 self.sync_ws_subscription(instrument_id);
656 Ok(())
657 }
658
659 fn subscribe_trades(&mut self, cmd: SubscribeTrades) -> anyhow::Result<()> {
660 let instrument_id = cmd.instrument_id;
661 self.ensure_live_subscription_allowed(instrument_id)?;
662 let cached = self.instruments.load().contains_key(&instrument_id);
663
664 if !cached && !self.config.auto_load_missing_instruments {
665 anyhow::bail!(
666 "Instrument {instrument_id} not found, and `auto_load_missing_instruments` is disabled"
667 );
668 }
669
670 if !self
671 .subscriptions
672 .add_live_subscription_intent(instrument_id, SubscriptionKind::Trades)
673 {
674 return Ok(());
675 }
676
677 if !cached {
678 self.queue_pending_load(instrument_id);
679 return Ok(());
680 }
681
682 self.sync_ws_subscription(instrument_id);
683 Ok(())
684 }
685
686 fn subscribe_instrument_status(
687 &mut self,
688 cmd: SubscribeInstrumentStatus,
689 ) -> anyhow::Result<()> {
690 self.subscribe_resolution(cmd.instrument_id, SubscriptionKind::Status)
691 }
692
693 fn subscribe_instrument_close(&mut self, cmd: SubscribeInstrumentClose) -> anyhow::Result<()> {
694 self.subscribe_resolution(cmd.instrument_id, SubscriptionKind::Close)
695 }
696
697 fn unsubscribe_book_deltas(&mut self, cmd: &UnsubscribeBookDeltas) -> anyhow::Result<()> {
698 let instrument_id = cmd.instrument_id;
699 let token_id = self
700 .subscriptions
701 .unsubscribe(instrument_id, SubscriptionKind::Deltas);
702 if let Some(token_id) = token_id {
703 self.sync_ws_subscription_for_token(instrument_id, token_id);
704 }
705
706 Ok(())
707 }
708
709 fn unsubscribe_quotes(&mut self, cmd: &UnsubscribeQuotes) -> anyhow::Result<()> {
710 let instrument_id = cmd.instrument_id;
711 let token_id = self
712 .subscriptions
713 .unsubscribe(instrument_id, SubscriptionKind::Quotes);
714 if let Some(token_id) = token_id {
715 self.sync_ws_subscription_for_token(instrument_id, token_id);
716 }
717
718 Ok(())
719 }
720
721 fn unsubscribe_trades(&mut self, cmd: &UnsubscribeTrades) -> anyhow::Result<()> {
722 let instrument_id = cmd.instrument_id;
723 let token_id = self
724 .subscriptions
725 .unsubscribe(instrument_id, SubscriptionKind::Trades);
726 if let Some(token_id) = token_id {
727 self.sync_ws_subscription_for_token(instrument_id, token_id);
728 }
729
730 Ok(())
731 }
732
733 fn unsubscribe_instrument_status(
734 &mut self,
735 cmd: &UnsubscribeInstrumentStatus,
736 ) -> anyhow::Result<()> {
737 let instrument_id = cmd.instrument_id;
738 let token_id = self
739 .subscriptions
740 .unsubscribe(instrument_id, SubscriptionKind::Status);
741 if let Some(token_id) = token_id {
742 self.sync_ws_subscription_for_token(instrument_id, token_id);
743 }
744 Ok(())
745 }
746
747 fn unsubscribe_instrument_close(
748 &mut self,
749 cmd: &UnsubscribeInstrumentClose,
750 ) -> anyhow::Result<()> {
751 let instrument_id = cmd.instrument_id;
752 let token_id = self
753 .subscriptions
754 .unsubscribe(instrument_id, SubscriptionKind::Close);
755 if let Some(token_id) = token_id {
756 self.sync_ws_subscription_for_token(instrument_id, token_id);
757 }
758 Ok(())
759 }
760
761 fn unsubscribe(&mut self, cmd: &UnsubscribeCustomData) -> anyhow::Result<()> {
762 if !is_supported_polybolt_data_type(&cmd.data_type) {
763 log::debug!(
764 "Ignoring unsupported Polymarket custom data unsubscription: {}",
765 cmd.data_type
766 );
767 return Ok(());
768 }
769
770 log::debug!(
771 "Tracking Polymarket PolyBolt custom data unsubscription: {}",
772 cmd.data_type
773 );
774 let changed = self.polybolt_feed.track_unsubscribe(&cmd.data_type)?;
775 if !changed {
776 return Ok(());
777 }
778
779 if !self.is_connected() {
780 return Ok(());
781 }
782
783 self.polybolt_feed
784 .request_reconcile(crate::polybolt::ReconcileReason::DesiredChanged);
785
786 Ok(())
787 }
788}
789
790fn clamp_new_market_fetch_max_concurrency(value: usize) -> usize {
791 value.clamp(1, NEW_MARKET_FETCH_MAX_CONCURRENCY_CAP)
792}
793
794pub(super) fn spawn_task<F>(tasks: &TaskSpawner, future: F)
795where
796 F: Future<Output = ()> + Send + 'static,
797{
798 if let Err(e) = tasks.spawn(future) {
799 log::debug!("Skipping Polymarket data task after shutdown began: {e}");
800 }
801}