1use std::{collections::HashMap, fmt::Debug, sync::Arc};
19
20use ahash::{AHashMap, AHashSet};
21use async_trait::async_trait;
22use nautilus_common::providers::{InstrumentProvider, InstrumentStore};
23use nautilus_model::{
24 identifiers::InstrumentId,
25 instruments::{Instrument, InstrumentAny},
26};
27use ustr::Ustr;
28
29use crate::{
30 common::consts::GAMMA_CONDITION_IDS_BATCH_SIZE,
31 config::PolymarketInstrumentProviderConfig,
32 filters::InstrumentFilter,
33 http::{gamma::PolymarketGammaHttpClient, models::GammaTag, query::GetGammaMarketsParams},
34};
35
36pub struct PolymarketInstrumentProvider {
44 store: InstrumentStore,
45 http_client: PolymarketGammaHttpClient,
46 token_index: AHashMap<Ustr, InstrumentId>,
47 filters: Vec<Arc<dyn InstrumentFilter>>,
48 config: PolymarketInstrumentProviderConfig,
49}
50
51impl Debug for PolymarketInstrumentProvider {
52 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53 f.debug_struct(stringify!(PolymarketInstrumentProvider))
54 .field("store", &self.store)
55 .field("http_client", &self.http_client)
56 .field("token_index_len", &self.token_index.len())
57 .field("filters", &self.filters)
58 .field("config", &self.config)
59 .finish()
60 }
61}
62
63impl PolymarketInstrumentProvider {
64 #[must_use]
66 pub fn new(
67 http_client: PolymarketGammaHttpClient,
68 config: Option<PolymarketInstrumentProviderConfig>,
69 ) -> Self {
70 Self {
71 store: InstrumentStore::new(),
72 http_client,
73 token_index: AHashMap::new(),
74 filters: Vec::new(),
75 config: config.unwrap_or_default(),
76 }
77 }
78
79 #[must_use]
81 pub fn with_filters(
82 http_client: PolymarketGammaHttpClient,
83 config: Option<PolymarketInstrumentProviderConfig>,
84 filters: Vec<Arc<dyn InstrumentFilter>>,
85 ) -> Self {
86 Self {
87 store: InstrumentStore::new(),
88 http_client,
89 token_index: AHashMap::new(),
90 filters,
91 config: config.unwrap_or_default(),
92 }
93 }
94
95 #[must_use]
97 pub fn with_filter(
98 http_client: PolymarketGammaHttpClient,
99 config: Option<PolymarketInstrumentProviderConfig>,
100 filter: Arc<dyn InstrumentFilter>,
101 ) -> Self {
102 Self {
103 store: InstrumentStore::new(),
104 http_client,
105 token_index: AHashMap::new(),
106 filters: vec![filter],
107 config: config.unwrap_or_default(),
108 }
109 }
110
111 pub fn add_filter(&mut self, filter: Arc<dyn InstrumentFilter>) {
113 self.filters.push(filter);
114 }
115
116 pub fn clear_filters(&mut self) {
118 self.filters.clear();
119 }
120
121 #[must_use]
123 pub fn get_by_token_id(&self, token_id: &Ustr) -> Option<&InstrumentAny> {
124 let instrument_id = self.token_index.get(token_id)?;
125 self.store.find(instrument_id)
126 }
127
128 #[must_use]
133 pub fn build_token_map(&self) -> AHashMap<Ustr, InstrumentAny> {
134 self.token_index
135 .iter()
136 .filter_map(|(token_id, instrument_id)| {
137 self.store
138 .find(instrument_id)
139 .map(|inst| (*token_id, inst.clone()))
140 })
141 .collect()
142 }
143
144 pub async fn load_by_slugs(&mut self, slugs: Vec<String>) -> anyhow::Result<()> {
154 let instruments = self.http_client.request_instruments_by_slugs(slugs).await?;
155
156 for instrument in &instruments {
157 self.token_index.insert(
158 Ustr::from(instrument.raw_symbol().as_str()),
159 instrument.id(),
160 );
161 }
162
163 self.store.add_bulk(instruments);
164
165 Ok(())
166 }
167
168 #[must_use]
170 pub fn filters(&self) -> Vec<Arc<dyn InstrumentFilter>> {
171 self.filters.clone()
172 }
173
174 #[must_use]
176 pub fn http_client(&self) -> &PolymarketGammaHttpClient {
177 &self.http_client
178 }
179
180 #[must_use]
182 pub fn config(&self) -> &PolymarketInstrumentProviderConfig {
183 &self.config
184 }
185
186 pub async fn list_tags(&self) -> anyhow::Result<Vec<GammaTag>> {
188 self.http_client.request_tags().await
189 }
190
191 pub fn add_instruments(&mut self, instruments: Vec<InstrumentAny>) {
192 for inst in &instruments {
193 self.token_index
194 .insert(Ustr::from(inst.raw_symbol().as_str()), inst.id());
195 }
196 self.store.add_bulk(instruments);
197 }
198
199 pub async fn load_by_event_slugs(&mut self, slugs: Vec<String>) -> anyhow::Result<()> {
205 let instruments = self
206 .http_client
207 .request_instruments_by_event_slugs(slugs)
208 .await?;
209 self.add_instruments(instruments);
210 Ok(())
211 }
212
213 pub async fn initialize(&mut self, reload: bool) -> anyhow::Result<()> {
215 if self.store.is_initialized() && !reload {
216 return Ok(());
217 }
218
219 if self.config.should_load_all() {
220 self.load_scoped_all().await?;
221 self.store.set_initialized();
222 return Ok(());
223 }
224
225 if self.config.has_load_ids() {
226 let load_ids = self.config.load_ids.clone().unwrap_or_default();
227 let filters = self.config.filters.clone();
228 self.load_ids(&load_ids, filters.as_ref()).await?;
229 self.store.set_initialized();
230 return Ok(());
231 }
232
233 if self.config.log_warnings {
234 log::warn!(
235 "No Polymarket instrument bootstrap configured: set instrument_config.load_all, instrument_config.load_ids, instrument_config.event_slugs, instrument_config.market_slugs, or instrument_config.event_slug_builder"
236 );
237 }
238 Ok(())
239 }
240
241 async fn load_scoped_all(&mut self) -> anyhow::Result<()> {
242 let has_explicit_slug_scope = self.config.event_slug_builder.is_some()
243 || self
244 .config
245 .event_slugs
246 .as_ref()
247 .is_some_and(|slugs| !slugs.is_empty())
248 || self
249 .config
250 .market_slugs
251 .as_ref()
252 .is_some_and(|slugs| !slugs.is_empty());
253 let event_slugs = self.resolve_event_slugs()?;
254 let market_slugs = self
255 .config
256 .market_slugs
257 .clone()
258 .unwrap_or_default()
259 .into_iter()
260 .filter(|slug| !slug.trim().is_empty())
261 .collect::<Vec<_>>();
262
263 if !event_slugs.is_empty() {
264 self.load_by_event_slugs(event_slugs).await?;
265 }
266
267 if !market_slugs.is_empty() {
268 self.load_by_slugs(market_slugs).await?;
269 }
270
271 if has_explicit_slug_scope {
272 return Ok(());
273 }
274
275 let filters = self.config.filters.clone();
276 self.load_all(filters.as_ref()).await
277 }
278
279 fn resolve_event_slugs(&self) -> anyhow::Result<Vec<String>> {
280 if let Some(builder) = self.config.event_slug_builder.as_ref() {
281 return builder.build_event_slugs();
282 }
283
284 Ok(self
285 .config
286 .event_slugs
287 .clone()
288 .unwrap_or_default()
289 .into_iter()
290 .filter(|slug| !slug.trim().is_empty())
291 .collect())
292 }
293
294 async fn load_filtered(&self) -> anyhow::Result<Vec<InstrumentAny>> {
297 fetch_instruments(&self.http_client, &self.filters).await
298 }
299}
300
301pub async fn fetch_instruments(
303 http_client: &PolymarketGammaHttpClient,
304 filters: &[Arc<dyn InstrumentFilter>],
305) -> anyhow::Result<Vec<InstrumentAny>> {
306 if filters.is_empty() {
307 return http_client.request_instruments().await;
308 }
309
310 let mut instruments = Vec::new();
311
312 for filter in filters {
313 if let Some(slugs) = filter.market_slugs()
314 && !slugs.is_empty()
315 {
316 let result = http_client.request_instruments_by_slugs(slugs).await?;
317 instruments.extend(result);
318 }
319
320 if let Some(event_slugs) = filter.event_slugs()
321 && !event_slugs.is_empty()
322 {
323 let result = http_client
324 .request_instruments_by_event_slugs(event_slugs)
325 .await?;
326 instruments.extend(result);
327 }
328
329 if let Some(params) = filter.query_params() {
330 let result = http_client.request_instruments_by_params(params).await?;
331 instruments.extend(result);
332 }
333
334 if let Some(event_queries) = filter.event_queries() {
335 for (event_slug, params) in event_queries {
336 let result = http_client
337 .request_instruments_by_event_query(&event_slug, params)
338 .await?;
339 instruments.extend(result);
340 }
341 }
342
343 if let Some(params) = filter.event_params() {
344 let result = http_client
345 .request_instruments_by_event_params(params)
346 .await?;
347 instruments.extend(result);
348 }
349
350 if let Some(params) = filter.search_params() {
351 let result = http_client.request_instruments_by_search(params).await?;
352 instruments.extend(result);
353 }
354 }
355
356 let mut seen = AHashSet::new();
357 instruments.retain(|inst| seen.insert(inst.id()));
358 instruments.retain(|inst| filters.iter().all(|f| f.accept(inst)));
359
360 Ok(instruments)
361}
362
363pub async fn fetch_configured_instruments(
366 http_client: &PolymarketGammaHttpClient,
367 config: &PolymarketInstrumentProviderConfig,
368 filters: &[Arc<dyn InstrumentFilter>],
369) -> anyhow::Result<Vec<InstrumentAny>> {
370 let mut instruments = Vec::new();
371
372 if config.should_load_all() {
373 let has_explicit_slug_scope = config.event_slug_builder.is_some()
374 || config
375 .event_slugs
376 .as_ref()
377 .is_some_and(|slugs| !slugs.is_empty())
378 || config
379 .market_slugs
380 .as_ref()
381 .is_some_and(|slugs| !slugs.is_empty());
382 let event_slugs = if let Some(builder) = config.event_slug_builder.as_ref() {
383 builder.build_event_slugs()?
384 } else {
385 config
386 .event_slugs
387 .clone()
388 .unwrap_or_default()
389 .into_iter()
390 .filter(|slug| !slug.trim().is_empty())
391 .collect::<Vec<_>>()
392 };
393
394 let market_slugs = config
395 .market_slugs
396 .clone()
397 .unwrap_or_default()
398 .into_iter()
399 .filter(|slug| !slug.trim().is_empty())
400 .collect::<Vec<_>>();
401
402 if !event_slugs.is_empty() {
403 instruments.extend(
404 http_client
405 .request_instruments_by_event_slugs(event_slugs)
406 .await?,
407 );
408 }
409
410 if !market_slugs.is_empty() {
411 instruments.extend(
412 http_client
413 .request_instruments_by_slugs(market_slugs)
414 .await?,
415 );
416 }
417
418 if has_explicit_slug_scope {
419 } else if filters.is_empty() {
421 if let Some(map) = config.filters.as_ref() {
422 if map.is_empty() {
423 instruments.extend(http_client.request_instruments().await?);
424 } else {
425 let params = build_gamma_params_from_hashmap(map);
426 instruments.extend(http_client.request_instruments_by_params(params).await?);
427 }
428 } else {
429 instruments.extend(http_client.request_instruments().await?);
430 }
431 } else {
432 instruments.extend(fetch_instruments(http_client, filters).await?);
433 }
434 } else if config.has_load_ids() {
435 let base_params = config
436 .filters
437 .as_ref()
438 .map(build_gamma_params_from_hashmap)
439 .unwrap_or_default();
440
441 let condition_ids = config
442 .load_ids
443 .clone()
444 .unwrap_or_default()
445 .into_iter()
446 .filter_map(|id| extract_condition_id(&id).ok())
447 .collect::<AHashSet<_>>()
448 .into_iter()
449 .collect::<Vec<_>>();
450
451 for chunk in condition_ids.chunks(GAMMA_CONDITION_IDS_BATCH_SIZE) {
452 let params = GetGammaMarketsParams {
453 condition_ids: Some(chunk.join(",")),
454 ..base_params.clone()
455 };
456 instruments.extend(http_client.request_instruments_by_params(params).await?);
457 }
458 }
459
460 let mut seen = AHashSet::new();
461 instruments.retain(|inst| seen.insert(inst.id()));
462 instruments.retain(|inst| filters.iter().all(|f| f.accept(inst)));
463 Ok(instruments)
464}
465
466pub fn extract_condition_id(instrument_id: &InstrumentId) -> anyhow::Result<String> {
472 let symbol = instrument_id.symbol.as_str();
473 symbol
474 .rfind('-')
475 .map(|idx| symbol[..idx].to_string())
476 .ok_or_else(|| {
477 anyhow::anyhow!("Cannot extract condition_id from symbol '{symbol}': no '-' separator")
478 })
479}
480
481pub fn build_gamma_params_from_hashmap(map: &HashMap<String, String>) -> GetGammaMarketsParams {
483 let mut params = GetGammaMarketsParams::default();
484
485 if let Some(v) = map.get("active") {
486 params.active = v.parse().ok();
487 }
488
489 if let Some(v) = map.get("closed") {
490 params.closed = v.parse().ok();
491 }
492
493 if let Some(v) = map.get("archived") {
494 params.archived = v.parse().ok();
495 }
496
497 if let Some(v) = map.get("slug") {
498 params.slug = Some(v.clone());
499 }
500
501 if let Some(v) = map.get("tag_id") {
502 params.tag_id = Some(v.clone());
503 }
504
505 if let Some(v) = map.get("condition_ids") {
506 params.condition_ids = Some(v.clone());
507 }
508
509 if let Some(v) = map.get("clob_token_ids") {
510 params.clob_token_ids = Some(v.clone());
511 }
512
513 if let Some(v) = map.get("liquidity_num_min") {
514 params.liquidity_num_min = v.parse().ok();
515 }
516
517 if let Some(v) = map.get("liquidity_num_max") {
518 params.liquidity_num_max = v.parse().ok();
519 }
520
521 if let Some(v) = map.get("volume_num_min") {
522 params.volume_num_min = v.parse().ok();
523 }
524
525 if let Some(v) = map.get("volume_num_max") {
526 params.volume_num_max = v.parse().ok();
527 }
528
529 if let Some(v) = map.get("order") {
530 params.order = Some(v.clone());
531 }
532
533 if let Some(v) = map.get("ascending") {
534 params.ascending = v.parse().ok();
535 }
536
537 if let Some(v) = map.get("limit") {
538 params.limit = v.parse().ok();
539 }
540
541 if let Some(v) = map.get("max_markets") {
542 params.max_markets = v.parse().ok();
543 }
544
545 params
546}
547
548pub async fn resolve_tag_slug(
550 client: &PolymarketGammaHttpClient,
551 slug: &str,
552) -> anyhow::Result<String> {
553 let tags = client.request_tags().await?;
554 tags.iter()
555 .find(|t| t.slug.as_deref() == Some(slug))
556 .map(|t| t.id.clone())
557 .ok_or_else(|| anyhow::anyhow!("Tag slug '{slug}' not found"))
558}
559
560#[async_trait(?Send)]
561impl InstrumentProvider for PolymarketInstrumentProvider {
562 fn store(&self) -> &InstrumentStore {
563 &self.store
564 }
565
566 fn store_mut(&mut self) -> &mut InstrumentStore {
567 &mut self.store
568 }
569
570 async fn load_all(&mut self, filters: Option<&HashMap<String, String>>) -> anyhow::Result<()> {
571 let instruments = if self.filters.is_empty() {
572 if let Some(map) = filters {
574 if map.is_empty() {
575 self.http_client.request_instruments().await?
576 } else {
577 let params = build_gamma_params_from_hashmap(map);
578 self.http_client
579 .request_instruments_by_params(params)
580 .await?
581 }
582 } else {
583 self.http_client.request_instruments().await?
584 }
585 } else {
586 self.load_filtered().await?
587 };
588
589 self.store.clear();
590 self.token_index.clear();
591 self.add_instruments(instruments);
592 self.store.set_initialized();
593
594 Ok(())
595 }
596
597 async fn load_ids(
598 &mut self,
599 instrument_ids: &[InstrumentId],
600 filters: Option<&HashMap<String, String>>,
601 ) -> anyhow::Result<()> {
602 let missing: Vec<_> = instrument_ids
603 .iter()
604 .filter(|id| !self.store.contains(id))
605 .collect();
606
607 if missing.is_empty() {
608 return Ok(());
609 }
610
611 let mut condition_ids: Vec<String> = missing
614 .iter()
615 .filter_map(|id| extract_condition_id(id).ok())
616 .collect();
617 condition_ids.sort();
618 condition_ids.dedup();
619
620 if condition_ids.is_empty() {
621 return Ok(());
622 }
623
624 let base_params = filters
625 .map(build_gamma_params_from_hashmap)
626 .unwrap_or_default();
627
628 for chunk in condition_ids.chunks(GAMMA_CONDITION_IDS_BATCH_SIZE) {
629 let params = GetGammaMarketsParams {
630 condition_ids: Some(chunk.join(",")),
631 ..base_params.clone()
632 };
633 let instruments = self
634 .http_client
635 .request_instruments_by_params(params)
636 .await?;
637 self.add_instruments(instruments);
638 }
639
640 Ok(())
641 }
642
643 async fn load(
644 &mut self,
645 instrument_id: &InstrumentId,
646 filters: Option<&HashMap<String, String>>,
647 ) -> anyhow::Result<()> {
648 if self.store.contains(instrument_id) {
649 return Ok(());
650 }
651
652 if let Ok(cid) = extract_condition_id(instrument_id) {
654 let params = GetGammaMarketsParams {
655 condition_ids: Some(cid),
656 ..Default::default()
657 };
658
659 if let Ok(instruments) = self.http_client.request_instruments_by_params(params).await {
660 self.add_instruments(instruments);
661
662 if self.store.contains(instrument_id) {
663 return Ok(());
664 }
665 }
666 }
667
668 if !self.store.is_initialized() {
670 self.load_all(filters).await?;
671 }
672
673 if self.store.contains(instrument_id) {
674 Ok(())
675 } else {
676 anyhow::bail!("Instrument {instrument_id} not found on Polymarket")
677 }
678 }
679}