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