1mod auto_load;
19mod dispatch;
20mod instruments;
21mod lifecycle;
22mod requests;
23mod runtime;
24mod subscriptions;
25
26use std::{
27 sync::{
28 Arc, Mutex as StdMutex,
29 atomic::{AtomicBool, Ordering},
30 },
31 time::Duration,
32};
33
34use ahash::AHashSet;
35use dashmap::DashMap;
36use nautilus_common::{
37 cache::InstrumentLookupError,
38 clients::DataClient,
39 live::{get_runtime, runner::get_data_event_sender},
40 messages::{
41 DataEvent,
42 data::{
43 RequestBookSnapshot, RequestCustomData, RequestInstrument, RequestInstruments,
44 RequestTrades, SubscribeBookDeltas, SubscribeCustomData, SubscribeInstruments,
45 SubscribeQuotes, SubscribeTrades, UnsubscribeBookDeltas, UnsubscribeCustomData,
46 UnsubscribeQuotes, UnsubscribeTrades,
47 },
48 },
49 msgbus::TypedHandler,
50};
51use nautilus_core::{
52 AtomicMap, AtomicSet,
53 time::{AtomicTime, get_atomic_clock_realtime},
54};
55use nautilus_model::{
56 data::QuoteTick,
57 enums::BookType,
58 events::PositionEvent,
59 identifiers::{ClientId, InstrumentId, Venue},
60 instruments::InstrumentAny,
61 orderbook::OrderBook,
62};
63use tokio::task::JoinHandle;
64use tokio_util::sync::CancellationToken;
65use ustr::Ustr;
66
67use self::{
68 instruments::TokenMeta,
69 requests::{
70 request_book_snapshot, request_data, request_instrument, request_instruments,
71 request_trades,
72 },
73 runtime::is_instrument_expired,
74 subscriptions::{resolve_token_id_from, sync_ws_subscription_async},
75};
76use crate::{
77 common::consts::POLYMARKET_VENUE,
78 config::PolymarketDataClientConfig,
79 filters::InstrumentFilter,
80 http::{
81 clob::PolymarketClobPublicClient, data_api::PolymarketDataApiHttpClient,
82 gamma::PolymarketGammaHttpClient,
83 },
84 providers::PolymarketInstrumentProvider,
85 resolve::ResolveWatchEntry,
86 rtds::{PolymarketRtdsFeed, is_supported_rtds_data_type},
87 websocket::client::PolymarketWebSocketClient,
88};
89
90const NEW_MARKET_FETCH_MAX_CONCURRENCY_CAP: usize = 64;
91pub(super) const NEW_MARKET_EMPTY_RECHECK_MAX_ATTEMPTS: usize = 1;
92pub(super) const NEW_MARKET_EMPTY_RECHECK_DELAY: Duration = Duration::from_millis(500);
93
94fn clamp_new_market_fetch_max_concurrency(value: usize) -> usize {
95 value.clamp(1, NEW_MARKET_FETCH_MAX_CONCURRENCY_CAP)
96}
97
98#[derive(Debug)]
106pub struct PolymarketDataClient {
107 clock: &'static AtomicTime,
108 client_id: ClientId,
109 config: PolymarketDataClientConfig,
110 provider: PolymarketInstrumentProvider,
111 clob_public_client: PolymarketClobPublicClient,
112 data_api_client: PolymarketDataApiHttpClient,
113 ws_client: PolymarketWebSocketClient,
114 is_connected: AtomicBool,
115 cancellation_token: CancellationToken,
116 tasks: Vec<JoinHandle<()>>,
117 data_sender: tokio::sync::mpsc::UnboundedSender<DataEvent>,
118 instruments: Arc<AtomicMap<InstrumentId, InstrumentAny>>,
119 token_meta: Arc<DashMap<Ustr, TokenMeta>>,
120 order_books: Arc<DashMap<InstrumentId, OrderBook>>,
121 last_quotes: Arc<DashMap<InstrumentId, QuoteTick>>,
122 active_quote_subs: Arc<AtomicSet<InstrumentId>>,
123 active_delta_subs: Arc<AtomicSet<InstrumentId>>,
124 active_trade_subs: Arc<AtomicSet<InstrumentId>>,
125 resolve_poll_watchlist: Arc<AtomicMap<String, ResolveWatchEntry>>,
126 resolve_watch_apply_mutex: Arc<StdMutex<()>>,
127 pending_snapshot_after_tick_change: Arc<AtomicSet<InstrumentId>>,
128 new_market_inflight_keys: Arc<DashMap<String, ()>>,
129 new_market_fetch_semaphore: Arc<tokio::sync::Semaphore>,
130 ws_open_tokens: Arc<AtomicSet<Ustr>>,
131 ws_sub_mutex: Arc<tokio::sync::Mutex<()>>,
132 pending_auto_loads: Arc<StdMutex<AHashSet<InstrumentId>>>,
133 auto_load_scheduled: Arc<AtomicBool>,
134 position_event_handler: Option<TypedHandler<PositionEvent>>,
135 rtds_feed: PolymarketRtdsFeed,
136}
137
138impl PolymarketDataClient {
139 pub fn new(
141 client_id: ClientId,
142 mut config: PolymarketDataClientConfig,
143 gamma_client: PolymarketGammaHttpClient,
144 clob_public_client: PolymarketClobPublicClient,
145 data_api_client: PolymarketDataApiHttpClient,
146 ws_client: PolymarketWebSocketClient,
147 ) -> Self {
148 let clock = get_atomic_clock_realtime();
149 let data_sender = get_data_event_sender();
150 let provider =
151 PolymarketInstrumentProvider::new(gamma_client, config.instrument_config.clone());
152 let configured_fetch_max_concurrency = config.new_market_fetch_max_concurrency;
153 let fetch_max_concurrency =
154 clamp_new_market_fetch_max_concurrency(configured_fetch_max_concurrency);
155
156 if configured_fetch_max_concurrency == 0 {
157 log::warn!(
158 "PolymarketDataClientConfig.new_market_fetch_max_concurrency=0 is invalid, clamping to 1"
159 );
160 } else if configured_fetch_max_concurrency > NEW_MARKET_FETCH_MAX_CONCURRENCY_CAP {
161 log::warn!(
162 "PolymarketDataClientConfig.new_market_fetch_max_concurrency={configured_fetch_max_concurrency} exceeds cap {NEW_MARKET_FETCH_MAX_CONCURRENCY_CAP}, clamping",
163 );
164 }
165 config.new_market_fetch_max_concurrency = fetch_max_concurrency;
166
167 let rtds_url = config.rtds_url();
168 let rtds_transport_backend = config.transport_backend;
169 let rtds_data_sender = data_sender.clone();
170
171 Self {
172 clock,
173 client_id,
174 config,
175 provider,
176 clob_public_client,
177 data_api_client,
178 ws_client,
179 is_connected: AtomicBool::new(false),
180 cancellation_token: CancellationToken::new(),
181 tasks: Vec::new(),
182 data_sender,
183 instruments: Arc::new(AtomicMap::new()),
184 token_meta: Arc::new(DashMap::new()),
185 order_books: Arc::new(DashMap::new()),
186 last_quotes: Arc::new(DashMap::new()),
187 active_quote_subs: Arc::new(AtomicSet::new()),
188 active_delta_subs: Arc::new(AtomicSet::new()),
189 active_trade_subs: Arc::new(AtomicSet::new()),
190 resolve_poll_watchlist: Arc::new(AtomicMap::new()),
191 resolve_watch_apply_mutex: Arc::new(StdMutex::new(())),
192 pending_snapshot_after_tick_change: Arc::new(AtomicSet::new()),
193 new_market_inflight_keys: Arc::new(DashMap::new()),
194 new_market_fetch_semaphore: Arc::new(tokio::sync::Semaphore::new(
195 fetch_max_concurrency,
196 )),
197 ws_open_tokens: Arc::new(AtomicSet::new()),
198 ws_sub_mutex: Arc::new(tokio::sync::Mutex::new(())),
199 pending_auto_loads: Arc::new(StdMutex::new(AHashSet::new())),
200 auto_load_scheduled: Arc::new(AtomicBool::new(false)),
201 position_event_handler: None,
202 rtds_feed: PolymarketRtdsFeed::new(
203 rtds_url,
204 rtds_transport_backend,
205 clock,
206 rtds_data_sender,
207 ),
208 }
209 }
210
211 #[must_use]
213 pub fn config(&self) -> &PolymarketDataClientConfig {
214 &self.config
215 }
216
217 #[must_use]
219 pub fn venue(&self) -> Venue {
220 *POLYMARKET_VENUE
221 }
222
223 #[must_use]
225 pub fn provider(&self) -> &PolymarketInstrumentProvider {
226 &self.provider
227 }
228
229 pub fn add_instrument_filter(&mut self, filter: Arc<dyn InstrumentFilter>) {
231 self.provider.add_filter(filter);
232 }
233
234 #[must_use]
236 pub fn is_connected(&self) -> bool {
237 self.is_connected.load(Ordering::Relaxed)
238 }
239
240 fn resolve_token_id(&self, instrument_id: InstrumentId) -> anyhow::Result<String> {
241 resolve_token_id_from(&self.instruments, instrument_id)
242 }
243
244 fn ensure_live_subscription_allowed(&self, instrument_id: InstrumentId) -> anyhow::Result<()> {
245 let now_ns = self.clock.get_time_ns();
246 let loaded = self.instruments.load();
247 let Some(instrument) = loaded.get(&instrument_id) else {
248 return Ok(());
249 };
250
251 if is_instrument_expired(instrument, now_ns) {
252 anyhow::bail!(
253 "Instrument {instrument_id} is expired and no longer available for live subscription"
254 );
255 }
256
257 Ok(())
258 }
259
260 fn ensure_market_data_request_allowed(
261 &self,
262 instrument_id: InstrumentId,
263 ) -> anyhow::Result<InstrumentAny> {
264 let loaded = self.instruments.load();
265 let instrument = loaded
266 .get(&instrument_id)
267 .ok_or_else(|| InstrumentLookupError::not_found(instrument_id))?
268 .clone();
269
270 if is_instrument_expired(&instrument, self.clock.get_time_ns()) {
271 anyhow::bail!(
272 "Instrument {instrument_id} is expired and no longer available for market data requests"
273 );
274 }
275
276 Ok(instrument)
277 }
278
279 fn sync_ws_subscription(&self, instrument_id: InstrumentId) {
284 let token_id_str = match self.resolve_token_id(instrument_id) {
285 Ok(s) => s,
286 Err(_) => return,
287 };
288 let active_quote_subs = self.active_quote_subs.clone();
289 let active_delta_subs = self.active_delta_subs.clone();
290 let active_trade_subs = self.active_trade_subs.clone();
291 let ws_open_tokens = self.ws_open_tokens.clone();
292 let ws_sub_mutex = self.ws_sub_mutex.clone();
293 let ws = self.ws_client.clone_subscription_handle();
294
295 get_runtime().spawn(sync_ws_subscription_async(
296 instrument_id,
297 token_id_str,
298 active_quote_subs,
299 active_delta_subs,
300 active_trade_subs,
301 ws_open_tokens,
302 ws_sub_mutex,
303 ws,
304 ));
305 }
306}
307
308#[async_trait::async_trait(?Send)]
309impl DataClient for PolymarketDataClient {
310 fn client_id(&self) -> ClientId {
311 self.client_id
312 }
313
314 fn venue(&self) -> Option<Venue> {
315 Some(*POLYMARKET_VENUE)
316 }
317
318 fn start(&mut self) -> anyhow::Result<()> {
319 self.start_client();
320 Ok(())
321 }
322
323 fn stop(&mut self) -> anyhow::Result<()> {
324 self.stop_client();
325 Ok(())
326 }
327
328 fn reset(&mut self) -> anyhow::Result<()> {
329 self.reset_client();
330 Ok(())
331 }
332
333 fn dispose(&mut self) -> anyhow::Result<()> {
334 self.stop()
335 }
336
337 async fn connect(&mut self) -> anyhow::Result<()> {
338 self.connect_client().await
339 }
340
341 async fn disconnect(&mut self) -> anyhow::Result<()> {
342 self.disconnect_client().await
343 }
344
345 fn is_connected(&self) -> bool {
346 self.is_connected.load(Ordering::Relaxed)
347 }
348
349 fn is_disconnected(&self) -> bool {
350 !self.is_connected()
351 }
352
353 fn request_data(&self, request: RequestCustomData) -> anyhow::Result<()> {
354 request_data(self, request);
355 Ok(())
356 }
357
358 fn request_instruments(&self, request: RequestInstruments) -> anyhow::Result<()> {
359 request_instruments(self, request);
360 Ok(())
361 }
362
363 fn request_instrument(&self, request: RequestInstrument) -> anyhow::Result<()> {
364 request_instrument(self, request);
365 Ok(())
366 }
367
368 fn request_book_snapshot(&self, request: RequestBookSnapshot) -> anyhow::Result<()> {
369 request_book_snapshot(self, request)
370 }
371
372 fn request_trades(&self, request: RequestTrades) -> anyhow::Result<()> {
373 request_trades(self, request)
374 }
375
376 fn subscribe_instruments(&mut self, _cmd: SubscribeInstruments) -> anyhow::Result<()> {
377 log::debug!("subscribe_instruments: subscribed individually via data subscription methods");
378 Ok(())
379 }
380
381 fn subscribe(&mut self, cmd: SubscribeCustomData) -> anyhow::Result<()> {
382 if !is_supported_rtds_data_type(&cmd.data_type) {
383 log::debug!(
384 "Ignoring unsupported Polymarket custom data subscription: {}",
385 cmd.data_type
386 );
387 return Ok(());
388 }
389
390 log::debug!(
391 "Tracking Polymarket RTDS custom data subscription: {}",
392 cmd.data_type
393 );
394 let changed = self.rtds_feed.track_subscribe(cmd.data_type)?;
395 if !changed {
396 return Ok(());
397 }
398
399 if !self.is_connected() {
400 return Ok(());
401 }
402
403 self.rtds_feed.schedule_sync();
404
405 Ok(())
406 }
407
408 fn subscribe_book_deltas(&mut self, cmd: SubscribeBookDeltas) -> anyhow::Result<()> {
409 if cmd.book_type != BookType::L2_MBP {
410 anyhow::bail!(
411 "Polymarket only supports L2_MBP order book deltas, received {:?}",
412 cmd.book_type
413 );
414 }
415
416 let instrument_id = cmd.instrument_id;
417 self.ensure_live_subscription_allowed(instrument_id)?;
418 let cached = self.instruments.load().contains_key(&instrument_id);
419
420 if !cached && !self.config.auto_load_missing_instruments {
421 anyhow::bail!(
422 "Instrument {instrument_id} not found, and `auto_load_missing_instruments` is disabled"
423 );
424 }
425
426 self.active_delta_subs.insert(instrument_id);
428 self.order_books
429 .entry(instrument_id)
430 .or_insert_with(|| OrderBook::new(instrument_id, BookType::L2_MBP));
431
432 if !cached {
433 self.queue_pending_load(instrument_id);
434 return Ok(());
435 }
436
437 self.sync_ws_subscription(instrument_id);
438 Ok(())
439 }
440
441 fn subscribe_quotes(&mut self, cmd: SubscribeQuotes) -> anyhow::Result<()> {
442 let instrument_id = cmd.instrument_id;
443 self.ensure_live_subscription_allowed(instrument_id)?;
444 let cached = self.instruments.load().contains_key(&instrument_id);
445
446 if !cached && !self.config.auto_load_missing_instruments {
447 anyhow::bail!(
448 "Instrument {instrument_id} not found, and `auto_load_missing_instruments` is disabled"
449 );
450 }
451
452 self.active_quote_subs.insert(instrument_id);
453
454 if !cached {
455 self.queue_pending_load(instrument_id);
456 return Ok(());
457 }
458
459 self.sync_ws_subscription(instrument_id);
460 Ok(())
461 }
462
463 fn subscribe_trades(&mut self, cmd: SubscribeTrades) -> anyhow::Result<()> {
464 let instrument_id = cmd.instrument_id;
465 self.ensure_live_subscription_allowed(instrument_id)?;
466 let cached = self.instruments.load().contains_key(&instrument_id);
467
468 if !cached && !self.config.auto_load_missing_instruments {
469 anyhow::bail!(
470 "Instrument {instrument_id} not found, and `auto_load_missing_instruments` is disabled"
471 );
472 }
473
474 self.active_trade_subs.insert(instrument_id);
475
476 if !cached {
477 self.queue_pending_load(instrument_id);
478 return Ok(());
479 }
480
481 self.sync_ws_subscription(instrument_id);
482 Ok(())
483 }
484
485 fn unsubscribe_book_deltas(&mut self, cmd: &UnsubscribeBookDeltas) -> anyhow::Result<()> {
486 let instrument_id = cmd.instrument_id;
487 self.active_delta_subs.remove(&instrument_id);
488 self.pending_snapshot_after_tick_change
489 .remove(&instrument_id);
490 self.drop_pending_if_unwanted(instrument_id);
491 self.drop_local_book_state_if_unwanted(instrument_id);
492 self.sync_ws_subscription(instrument_id);
493 Ok(())
494 }
495
496 fn unsubscribe_quotes(&mut self, cmd: &UnsubscribeQuotes) -> anyhow::Result<()> {
497 let instrument_id = cmd.instrument_id;
498 self.active_quote_subs.remove(&instrument_id);
499 self.drop_pending_if_unwanted(instrument_id);
500 self.drop_local_book_state_if_unwanted(instrument_id);
501 self.sync_ws_subscription(instrument_id);
502 Ok(())
503 }
504
505 fn unsubscribe_trades(&mut self, cmd: &UnsubscribeTrades) -> anyhow::Result<()> {
506 let instrument_id = cmd.instrument_id;
507 self.active_trade_subs.remove(&instrument_id);
508 self.drop_pending_if_unwanted(instrument_id);
509 self.sync_ws_subscription(instrument_id);
510 Ok(())
511 }
512
513 fn unsubscribe(&mut self, cmd: &UnsubscribeCustomData) -> anyhow::Result<()> {
514 if !is_supported_rtds_data_type(&cmd.data_type) {
515 log::debug!(
516 "Ignoring unsupported Polymarket custom data unsubscription: {}",
517 cmd.data_type
518 );
519 return Ok(());
520 }
521
522 log::debug!(
523 "Tracking Polymarket RTDS custom data unsubscription: {}",
524 cmd.data_type
525 );
526 let changed = self.rtds_feed.track_unsubscribe(&cmd.data_type)?;
527 if !changed {
528 return Ok(());
529 }
530
531 if !self.is_connected() {
532 return Ok(());
533 }
534
535 self.rtds_feed.schedule_sync();
536
537 Ok(())
538 }
539}