1use std::{collections::HashMap, result::Result as StdResult, sync::Arc};
29
30use nautilus_core::{
31 UnixNanos,
32 consts::NAUTILUS_USER_AGENT,
33 time::{AtomicTime, get_atomic_clock_realtime},
34};
35use nautilus_model::instruments::InstrumentAny;
36use nautilus_network::{
37 http::{HttpClient, HttpClientError, HttpResponse, Method, USER_AGENT},
38 retry::{RetryConfig, RetryManager},
39 websocket::proxy::ProxyUrl,
40};
41use serde::{Deserialize, Serialize, de::DeserializeOwned};
42use serde_json::Value;
43
44use crate::{
45 common::urls::gamma_api_url,
46 filters::set_market_closed,
47 http::{
48 error::{Error, Result},
49 models::{GammaEvent, GammaMarket, GammaTag, SearchResponse},
50 pagination::{Completion, CursorProtocol, FetchOutcome, Paginator, WindowedCollect},
51 parse::{create_instrument_from_def, parse_gamma_market},
52 query::{GetGammaEventsParams, GetGammaMarketsParams, GetSearchParams},
53 rate_limits::POLYMARKET_GAMMA_REST_QUOTA,
54 },
55};
56
57const GAMMA_MARKETS_KEYSET_PAGE_LIMIT: u32 = 100;
58const GAMMA_EVENTS_KEYSET_PAGE_LIMIT: u32 = 500;
59
60#[derive(Clone, Copy, Debug, Eq, PartialEq)]
61enum GammaStop {
62 CallerCapped,
63}
64
65#[derive(Debug, Clone)]
70pub struct PolymarketGammaRawHttpClient {
71 client: HttpClient,
72 base_url: String,
73}
74
75impl PolymarketGammaRawHttpClient {
76 pub fn new(base_url: Option<String>, timeout_secs: u64) -> StdResult<Self, HttpClientError> {
82 Self::new_with_proxy(base_url, timeout_secs, None)
83 }
84
85 pub fn new_with_proxy(
91 base_url: Option<String>,
92 timeout_secs: u64,
93 proxy_url: Option<ProxyUrl>,
94 ) -> StdResult<Self, HttpClientError> {
95 Ok(Self {
96 client: HttpClient::builder()
97 .headers(Self::default_headers())
98 .default_quota(*POLYMARKET_GAMMA_REST_QUOTA)
99 .timeout_secs(timeout_secs)
100 .maybe_proxy_url(proxy_url.map(|url| url.expose().to_string()))
101 .build()?,
102 base_url: base_url
103 .unwrap_or_else(|| gamma_api_url().to_string())
104 .trim_end_matches('/')
105 .to_string(),
106 })
107 }
108
109 fn default_headers() -> HashMap<String, String> {
110 HashMap::from([
111 (USER_AGENT.to_string(), NAUTILUS_USER_AGENT.to_string()),
112 ("Content-Type".to_string(), "application/json".to_string()),
113 ])
114 }
115
116 fn url(&self, path: &str) -> String {
117 format!("{}{path}", self.base_url)
118 }
119
120 async fn send_get<P: Serialize, T: DeserializeOwned>(
121 &self,
122 path: &str,
123 params: Option<&P>,
124 ) -> Result<T> {
125 let url = self.url(path);
126 let response = self
127 .client
128 .request_with_params(Method::GET, url, params, None, None, None, None)
129 .await
130 .map_err(Error::from_http_client)?;
131
132 decode_response(&response)
133 }
134
135 async fn send_get_query_map<T: DeserializeOwned>(
136 &self,
137 path: &str,
138 params: Option<&HashMap<String, Vec<String>>>,
139 ) -> Result<T> {
140 let url = self.url(path);
141 let response = self
142 .client
143 .request(Method::GET, url, params, None, None, None, None)
144 .await
145 .map_err(Error::from_http_client)?;
146
147 decode_response(&response)
148 }
149
150 pub async fn get_gamma_markets(
154 &self,
155 params: GetGammaMarketsParams,
156 ) -> Result<Vec<GammaMarket>> {
157 let query_params = gamma_markets_query_params(params)?;
158 let value: Value = self
159 .send_get_query_map("/markets", Some(&query_params))
160 .await?;
161
162 let array = match value {
163 Value::Array(_) => value,
164 Value::Object(ref map) if map.contains_key("data") => {
165 map.get("data").cloned().unwrap_or(Value::Array(vec![]))
166 }
167 _ => {
168 return Err(Error::decode(
169 "Unrecognized Gamma markets response schema".to_string(),
170 ));
171 }
172 };
173
174 serde_json::from_value(array).map_err(Error::Serde)
175 }
176
177 async fn get_gamma_markets_keyset(
178 &self,
179 mut params: GetGammaMarketsParams,
180 after_cursor: Option<&str>,
181 ) -> Result<GammaMarketsKeysetResponse> {
182 params.validate_keyset().map_err(Error::decode)?;
183 params.offset = None;
184 let mut query_params = gamma_markets_query_params(params)?;
185 if let Some(after_cursor) = after_cursor {
186 query_params.insert("after_cursor".to_string(), vec![after_cursor.to_string()]);
187 }
188 self.send_get_query_map("/markets/keyset", Some(&query_params))
189 .await
190 }
191
192 pub async fn get_gamma_market(&self, market_id: &str) -> Result<GammaMarket> {
194 let path = format!("/markets/{market_id}");
195 self.send_get::<(), _>(&path, None::<&()>).await
196 }
197
198 pub async fn get_gamma_market_by_slug(&self, slug: &str) -> Result<GammaMarket> {
200 let path = format!("/markets/slug/{slug}");
201 self.send_get::<(), _>(&path, None::<&()>).await
202 }
203
204 pub async fn get_gamma_events_by_slug(&self, slug: &str) -> Result<Vec<GammaEvent>> {
206 #[derive(Serialize)]
207 struct EventSlugParams<'a> {
208 slug: &'a str,
209 }
210 let params = EventSlugParams { slug };
211 self.send_get("/events", Some(¶ms)).await
212 }
213
214 pub async fn get_gamma_events(&self, params: GetGammaEventsParams) -> Result<Vec<GammaEvent>> {
216 let query_params = gamma_events_query_params(params)?;
217 self.send_get_query_map("/events", Some(&query_params))
218 .await
219 }
220
221 async fn get_gamma_events_keyset(
222 &self,
223 mut params: GetGammaEventsParams,
224 after_cursor: Option<&str>,
225 ) -> Result<GammaEventsKeysetResponse> {
226 params.validate_keyset().map_err(Error::decode)?;
227 params.offset = None;
228 let mut query_params = gamma_events_query_params(params)?;
229 if let Some(after_cursor) = after_cursor {
230 query_params.insert("after_cursor".to_string(), vec![after_cursor.to_string()]);
231 }
232 self.send_get_query_map("/events/keyset", Some(&query_params))
233 .await
234 }
235
236 pub async fn get_gamma_tags(&self) -> Result<Vec<GammaTag>> {
238 self.send_get::<(), _>("/tags", None::<&()>).await
239 }
240
241 pub async fn get_public_search(&self, params: GetSearchParams) -> Result<SearchResponse> {
243 self.send_get("/public-search", Some(¶ms)).await
244 }
245}
246
247#[derive(Debug, Deserialize)]
248struct GammaMarketsKeysetResponse {
249 markets: Vec<GammaMarket>,
250 next_cursor: Option<String>,
251}
252
253#[derive(Debug, Deserialize)]
254struct GammaEventsKeysetResponse {
255 events: Vec<GammaEvent>,
256 next_cursor: Option<String>,
257}
258
259fn decode_response<T: DeserializeOwned>(response: &HttpResponse) -> Result<T> {
260 if response.status.is_success() {
261 serde_json::from_slice(&response.body).map_err(Error::Serde)
262 } else {
263 Err(Error::from_status_code(
264 response.status.as_u16(),
265 &response.body,
266 ))
267 }
268}
269
270fn gamma_markets_query_params(
271 params: GetGammaMarketsParams,
272) -> Result<HashMap<String, Vec<String>>> {
273 let mut scalar_params = params;
274 let id = scalar_params.id.take();
275 let slug = scalar_params.slug.take();
276 let clob_token_ids = scalar_params.clob_token_ids.take();
277 let condition_ids = scalar_params.condition_ids.take();
278 let question_ids = scalar_params.question_ids.take();
279 let market_maker_address = scalar_params.market_maker_address.take();
280 let tag_id = scalar_params.tag_id.take();
281 let sports_market_types = scalar_params.sports_market_types.take();
282 let value = serde_json::to_value(&scalar_params).map_err(Error::Serde)?;
283 let fields = value
284 .as_object()
285 .ok_or_else(|| Error::decode("Gamma markets params must encode to an object"))?;
286 let mut params = HashMap::with_capacity(fields.len());
287
288 for (key, value) in fields {
289 if let Some(value) = gamma_query_value(value)? {
290 params.insert(key.clone(), vec![value]);
291 }
292 }
293
294 insert_repeated_param(&mut params, "id", id);
295 insert_repeated_param(&mut params, "slug", slug);
296 insert_repeated_param(&mut params, "clob_token_ids", clob_token_ids);
297 insert_repeated_param(&mut params, "condition_ids", condition_ids);
298 insert_repeated_param(&mut params, "question_ids", question_ids);
299 insert_repeated_param(&mut params, "market_maker_address", market_maker_address);
300 insert_repeated_param(&mut params, "tag_id", tag_id);
301 insert_repeated_param(&mut params, "sports_market_types", sports_market_types);
302
303 Ok(params)
304}
305
306fn gamma_events_query_params(params: GetGammaEventsParams) -> Result<HashMap<String, Vec<String>>> {
307 let mut scalar_params = params;
308 let id = scalar_params.id.take();
309 let slug = scalar_params.slug.take();
310 let tag_id = scalar_params.tag_id.take();
311 let exclude_tag_id = scalar_params.exclude_tag_id.take();
312 let series_id = scalar_params.series_id.take();
313 let game_id = scalar_params.game_id.take();
314 let created_by = scalar_params.created_by.take();
315 let value = serde_json::to_value(&scalar_params).map_err(Error::Serde)?;
316 let fields = value
317 .as_object()
318 .ok_or_else(|| Error::decode("Gamma events params must encode to an object"))?;
319 let mut params = HashMap::with_capacity(fields.len());
320
321 for (key, value) in fields {
322 if let Some(value) = gamma_query_value(value)? {
323 params.insert(key.clone(), vec![value]);
324 }
325 }
326
327 insert_repeated_param(&mut params, "id", id);
328 insert_repeated_param(&mut params, "slug", slug);
329 insert_repeated_param(&mut params, "tag_id", tag_id);
330 insert_repeated_param(&mut params, "exclude_tag_id", exclude_tag_id);
331 insert_repeated_param(&mut params, "series_id", series_id);
332 insert_repeated_param(&mut params, "game_id", game_id);
333 insert_repeated_param(&mut params, "created_by", created_by);
334
335 Ok(params)
336}
337
338fn insert_repeated_param<T: ToString>(
339 params: &mut HashMap<String, Vec<String>>,
340 key: &str,
341 values: Option<Vec<T>>,
342) {
343 let Some(values) = values else {
344 return;
345 };
346
347 params.insert(
348 key.to_string(),
349 values
350 .into_iter()
351 .map(|value| value.to_string().trim().to_string())
352 .collect(),
353 );
354}
355
356fn gamma_query_value(value: &Value) -> Result<Option<String>> {
357 match value {
358 Value::Null => Ok(None),
359 Value::String(value) => Ok(Some(value.clone())),
360 Value::Bool(value) => Ok(Some(value.to_string())),
361 Value::Number(value) => Ok(Some(value.to_string())),
362 other => Err(Error::decode(format!(
363 "Unsupported Gamma query value: {other}"
364 ))),
365 }
366}
367
368fn parse_markets_to_instruments(markets: &[GammaMarket], ts_init: UnixNanos) -> Vec<InstrumentAny> {
369 let (instruments, _transient) = parse_markets_with_transient(markets, ts_init);
370 instruments
371}
372
373fn parse_markets_with_transient(
382 markets: &[GammaMarket],
383 ts_init: UnixNanos,
384) -> (Vec<InstrumentAny>, Vec<String>) {
385 let mut instruments = Vec::new();
386 let mut transient = Vec::new();
387
388 for market in markets {
389 if is_transient_clob_token_ids(&market.clob_token_ids) {
390 transient.push(market.condition_id.clone());
391 continue;
392 }
393
394 match parse_gamma_market(market) {
395 Ok(defs) => {
396 for def in defs {
397 match create_instrument_from_def(&def, ts_init) {
398 Ok(InstrumentAny::BinaryOption(mut binary)) => {
399 set_market_closed(&mut binary, def.closed);
400 instruments.push(InstrumentAny::BinaryOption(binary));
401 }
402 Ok(other) => instruments.push(other),
403 Err(e) => log::warn!("Failed to create instrument: {e}"),
404 }
405 }
406 }
407 Err(e) => log::warn!("Failed to parse gamma market: {e}"),
408 }
409 }
410
411 if !transient.is_empty() {
412 log::debug!(
413 "{} market(s) without usable clob_token_ids deferred as transient (CLOB hydration)",
414 transient.len(),
415 );
416 }
417 (instruments, transient)
418}
419
420fn is_transient_clob_token_ids(raw: &str) -> bool {
424 if raw.is_empty() {
425 return true;
426 }
427
428 match serde_json::from_str::<Vec<String>>(raw) {
429 Ok(ids) => ids.is_empty() || ids.iter().any(|t| t.is_empty()),
430 Err(_) => false,
431 }
432}
433
434fn flatten_event_markets(events: Vec<GammaEvent>) -> Vec<GammaMarket> {
435 events
436 .into_iter()
437 .flat_map(|event| {
438 let event_game_id = event.game_id;
439 event.markets.into_iter().map(move |mut market| {
440 if market.game_id.is_none() {
441 market.game_id.clone_from(&event_game_id);
442 }
443 market
444 })
445 })
446 .collect()
447}
448
449#[derive(Debug, Clone)]
455pub struct PolymarketGammaHttpClient {
456 inner: Arc<PolymarketGammaRawHttpClient>,
457 clock: &'static AtomicTime,
458 retry_manager: Arc<RetryManager<Error>>,
459}
460
461impl PolymarketGammaHttpClient {
462 pub fn new(
468 gamma_base_url: Option<String>,
469 timeout_secs: u64,
470 retry_config: RetryConfig,
471 ) -> StdResult<Self, HttpClientError> {
472 Self::new_with_proxy(gamma_base_url, timeout_secs, retry_config, None)
473 }
474
475 pub fn new_with_proxy(
481 gamma_base_url: Option<String>,
482 timeout_secs: u64,
483 retry_config: RetryConfig,
484 proxy_url: Option<ProxyUrl>,
485 ) -> StdResult<Self, HttpClientError> {
486 Ok(Self {
487 inner: Arc::new(PolymarketGammaRawHttpClient::new_with_proxy(
488 gamma_base_url,
489 timeout_secs,
490 proxy_url,
491 )?),
492 clock: get_atomic_clock_realtime(),
493 retry_manager: Arc::new(RetryManager::new(retry_config)),
494 })
495 }
496
497 async fn fetch_gamma_markets_paginated(
499 &self,
500 base_params: GetGammaMarketsParams,
501 ) -> anyhow::Result<Vec<GammaMarket>> {
502 let page_size = base_params
503 .limit
504 .unwrap_or(GAMMA_MARKETS_KEYSET_PAGE_LIMIT)
505 .min(GAMMA_MARKETS_KEYSET_PAGE_LIMIT);
506 let protocol = CursorProtocol::<GammaStop>::gamma("Gamma market");
507 let reducer = WindowedCollect::new(
508 base_params.offset.unwrap_or(0) as usize,
509 base_params.max_markets.map(|value| value as usize),
510 GammaStop::CallerCapped,
511 );
512 let paginator = Paginator::new("Gamma market", protocol, reducer);
513 let completed = paginator
514 .run(
515 |position| {
516 let after_cursor = position.map(|cursor| cursor.as_ref().to_string());
517 let params = GetGammaMarketsParams {
518 limit: Some(page_size),
519 offset: None,
520 ..base_params.clone()
521 };
522 async move {
523 let response = self
524 .inner
525 .get_gamma_markets_keyset(params, after_cursor.as_deref())
526 .await?;
527 Ok::<_, anyhow::Error>(FetchOutcome::Page {
528 rows: response.markets,
529 wire: response.next_cursor,
530 })
531 }
532 },
533 anyhow::Error::new,
534 )
535 .await?;
536
537 match completed.completion {
538 Completion::WireExhausted | Completion::Stopped(GammaStop::CallerCapped) => {
539 Ok(completed.output)
540 }
541 }
542 }
543
544 async fn fetch_all_gamma_markets(&self) -> anyhow::Result<Vec<GammaMarket>> {
546 self.fetch_gamma_markets_paginated(GetGammaMarketsParams {
547 active: Some(true),
548 closed: Some(false),
549 ..Default::default()
550 })
551 .await
552 }
553
554 pub async fn request_instruments(&self) -> anyhow::Result<Vec<InstrumentAny>> {
560 let markets = self.fetch_all_gamma_markets().await?;
561 let ts_init = self.clock.get_time_ns();
562 let instruments = parse_markets_to_instruments(&markets, ts_init);
563 log::debug!("Parsed {} instruments from Gamma API", instruments.len());
564 Ok(instruments)
565 }
566
567 pub async fn request_instruments_by_slugs(
577 &self,
578 slugs: Vec<String>,
579 ) -> anyhow::Result<Vec<InstrumentAny>> {
580 let ts_init = self.clock.get_time_ns();
581
582 let futures = slugs.into_iter().map(|slug| {
583 let inner = Arc::clone(&self.inner);
584 async move {
585 let params = GetGammaMarketsParams {
586 slug: Some(vec![slug.clone()]),
587 ..Default::default()
588 };
589
590 match inner.get_gamma_markets(params).await {
591 Ok(markets) => Some((slug, markets)),
592 Err(e) => {
593 log::warn!("Failed to fetch slug '{slug}': {e}");
594 None
595 }
596 }
597 }
598 });
599
600 let results = futures_util::future::join_all(futures).await;
601
602 let total_slugs = results.len();
603 let succeeded = results.iter().filter(|r| r.is_some()).count();
604 let mut instruments = Vec::new();
605
606 for result in results.into_iter().flatten() {
607 let (slug, markets) = result;
608 if markets.is_empty() {
609 log::debug!("No markets found for slug '{slug}'");
610 continue;
611 }
612 instruments.extend(parse_markets_to_instruments(&markets, ts_init));
613 }
614
615 if succeeded == 0 && total_slugs > 0 {
616 anyhow::bail!("All {total_slugs} slug requests failed");
617 }
618
619 log::debug!("Parsed {} instruments from slug queries", instruments.len());
620 Ok(instruments)
621 }
622
623 pub async fn request_instruments_by_slugs_with_retry(
630 &self,
631 slugs: Vec<String>,
632 ) -> anyhow::Result<Vec<InstrumentAny>> {
633 let inner = Arc::clone(&self.inner);
634 let ts_init = self.clock.get_time_ns();
635
636 self.retry_manager
637 .execute_with_retry(
638 "gamma_fetch_by_slugs",
639 || {
640 let inner = Arc::clone(&inner);
641 let slugs = slugs.clone();
642 async move {
643 let futures = slugs.into_iter().map(|slug| {
644 let inner = Arc::clone(&inner);
645 async move {
646 let params = GetGammaMarketsParams {
647 slug: Some(vec![slug.clone()]),
648 ..Default::default()
649 };
650 inner
651 .get_gamma_markets(params)
652 .await
653 .map(|markets| (slug, markets))
654 }
655 });
656
657 let results: Vec<_> = futures_util::future::join_all(futures)
658 .await
659 .into_iter()
660 .collect::<StdResult<Vec<_>, _>>()?;
661
662 let instruments: Vec<InstrumentAny> = results
663 .into_iter()
664 .flat_map(|(_, markets)| {
665 parse_markets_to_instruments(&markets, ts_init)
666 })
667 .collect();
668
669 if instruments.is_empty() {
670 return Err(Error::transport(
671 "Gamma returned no instruments (indexing lag)",
672 ));
673 }
674
675 Ok(instruments)
676 }
677 },
678 |e| e.is_retryable(),
679 |e| Error::transport(e.to_string()),
680 )
681 .await
682 .map_err(|e| anyhow::anyhow!("{e}"))
683 }
684
685 pub async fn request_instruments_by_event_slugs(
690 &self,
691 event_slugs: Vec<String>,
692 ) -> anyhow::Result<Vec<InstrumentAny>> {
693 let ts_init = self.clock.get_time_ns();
694
695 let futures = event_slugs.into_iter().map(|slug| {
696 let inner = Arc::clone(&self.inner);
697 async move {
698 match inner.get_gamma_events_by_slug(&slug).await {
699 Ok(events) => Some((slug, events)),
700 Err(e) => {
701 log::warn!("Failed to fetch event slug '{slug}': {e}");
702 None
703 }
704 }
705 }
706 });
707
708 let results = futures_util::future::join_all(futures).await;
709
710 let total = results.len();
711 let succeeded = results.iter().filter(|r| r.is_some()).count();
712 let mut instruments = Vec::new();
713
714 for result in results.into_iter().flatten() {
715 let (slug, events) = result;
716 let markets = flatten_event_markets(events);
717 if markets.is_empty() {
718 log::warn!("No markets found in event slug '{slug}'");
719 continue;
720 }
721 instruments.extend(parse_markets_to_instruments(&markets, ts_init));
722 }
723
724 if succeeded == 0 && total > 0 {
725 anyhow::bail!("All {total} event slug requests failed");
726 }
727
728 log::debug!(
729 "Parsed {} instruments from event slug queries",
730 instruments.len()
731 );
732 Ok(instruments)
733 }
734
735 pub async fn request_instruments_by_params(
737 &self,
738 base_params: GetGammaMarketsParams,
739 ) -> anyhow::Result<Vec<InstrumentAny>> {
740 let markets = self.fetch_gamma_markets_paginated(base_params).await?;
741 let ts_init = self.clock.get_time_ns();
742 let instruments = parse_markets_to_instruments(&markets, ts_init);
743 log::debug!("Parsed {} instruments from params query", instruments.len());
744 Ok(instruments)
745 }
746
747 pub async fn request_instruments_by_params_with_transient(
753 &self,
754 base_params: GetGammaMarketsParams,
755 ) -> anyhow::Result<(Vec<InstrumentAny>, Vec<String>)> {
756 let markets = self.fetch_gamma_markets_paginated(base_params).await?;
757 let ts_init = self.clock.get_time_ns();
758 let (instruments, transient) = parse_markets_with_transient(&markets, ts_init);
759 log::debug!(
760 "Parsed {} instruments and {} transient condition_id(s) from params query",
761 instruments.len(),
762 transient.len(),
763 );
764 Ok((instruments, transient))
765 }
766
767 pub async fn request_markets_by_params(
769 &self,
770 base_params: GetGammaMarketsParams,
771 ) -> anyhow::Result<Vec<GammaMarket>> {
772 self.fetch_gamma_markets_paginated(base_params).await
773 }
774
775 pub async fn request_instruments_by_event_query(
784 &self,
785 event_slug: &str,
786 params: GetGammaMarketsParams,
787 ) -> anyhow::Result<Vec<InstrumentAny>> {
788 let events = self.inner.get_gamma_events_by_slug(event_slug).await?;
789 let mut markets = flatten_event_markets(events);
790
791 if markets.is_empty() {
792 log::warn!("No markets found in event slug '{event_slug}'");
793 return Ok(Vec::new());
794 }
795
796 log::debug!("Event '{event_slug}' returned {} markets", markets.len());
797
798 if let Some(ref order_field) = params.order {
800 let ascending = params.ascending.unwrap_or(false);
801 markets.sort_by(|a, b| {
802 let cmp = match order_field.as_str() {
803 "liquidity" => a
804 .liquidity_num
805 .unwrap_or(0.0)
806 .partial_cmp(&b.liquidity_num.unwrap_or(0.0)),
807 "volume" => a
808 .volume_num
809 .unwrap_or(0.0)
810 .partial_cmp(&b.volume_num.unwrap_or(0.0)),
811 "volume24hr" => a
812 .volume_24hr
813 .unwrap_or(0.0)
814 .partial_cmp(&b.volume_24hr.unwrap_or(0.0)),
815 "competitive" => a
816 .competitive
817 .unwrap_or(0.0)
818 .partial_cmp(&b.competitive.unwrap_or(0.0)),
819 "spread" => a
820 .spread
821 .unwrap_or(f64::MAX)
822 .partial_cmp(&b.spread.unwrap_or(f64::MAX)),
823 "best_bid" => a
824 .best_bid
825 .unwrap_or(0.0)
826 .partial_cmp(&b.best_bid.unwrap_or(0.0)),
827 "one_day_price_change" => a
828 .one_day_price_change
829 .unwrap_or(0.0)
830 .partial_cmp(&b.one_day_price_change.unwrap_or(0.0)),
831 "volume_1wk" => a
832 .volume_1wk
833 .unwrap_or(0.0)
834 .partial_cmp(&b.volume_1wk.unwrap_or(0.0)),
835 _ => None,
836 };
837 let cmp = cmp.unwrap_or(std::cmp::Ordering::Equal);
838 if ascending { cmp } else { cmp.reverse() }
839 });
840 }
841
842 if let Some(cap) = params.max_markets {
844 markets.truncate(cap as usize);
845 }
846
847 let ts_init = self.clock.get_time_ns();
848 let instruments = parse_markets_to_instruments(&markets, ts_init);
849 log::debug!(
850 "Parsed {} instruments from event query '{event_slug}'",
851 instruments.len()
852 );
853 Ok(instruments)
854 }
855
856 async fn fetch_gamma_events_paginated(
858 &self,
859 base_params: GetGammaEventsParams,
860 ) -> anyhow::Result<Vec<GammaEvent>> {
861 let page_size = base_params
862 .limit
863 .unwrap_or(GAMMA_EVENTS_KEYSET_PAGE_LIMIT)
864 .min(GAMMA_EVENTS_KEYSET_PAGE_LIMIT);
865 let protocol = CursorProtocol::<GammaStop>::gamma("Gamma event");
866 let reducer = WindowedCollect::new(
867 base_params.offset.unwrap_or(0) as usize,
868 base_params.max_events.map(|value| value as usize),
869 GammaStop::CallerCapped,
870 );
871 let paginator = Paginator::new("Gamma event", protocol, reducer);
872 let completed = paginator
873 .run(
874 |position| {
875 let after_cursor = position.map(|cursor| cursor.as_ref().to_string());
876 let params = GetGammaEventsParams {
877 limit: Some(page_size),
878 offset: None,
879 ..base_params.clone()
880 };
881 async move {
882 let response = self
883 .inner
884 .get_gamma_events_keyset(params, after_cursor.as_deref())
885 .await?;
886 Ok::<_, anyhow::Error>(FetchOutcome::Page {
887 rows: response.events,
888 wire: response.next_cursor,
889 })
890 }
891 },
892 anyhow::Error::new,
893 )
894 .await?;
895
896 match completed.completion {
897 Completion::WireExhausted | Completion::Stopped(GammaStop::CallerCapped) => {
898 Ok(completed.output)
899 }
900 }
901 }
902
903 pub async fn request_instruments_by_event_params(
905 &self,
906 params: GetGammaEventsParams,
907 ) -> anyhow::Result<Vec<InstrumentAny>> {
908 let events = self.fetch_gamma_events_paginated(params).await?;
909 let ts_init = self.clock.get_time_ns();
910 let total_events = events.len();
911 let markets = flatten_event_markets(events);
912 let total_markets = markets.len();
913 let instruments = parse_markets_to_instruments(&markets, ts_init);
914 log::debug!(
915 "Parsed {} instruments from {total_events} events ({total_markets} markets)",
916 instruments.len(),
917 );
918 Ok(instruments)
919 }
920
921 pub async fn request_events_by_params(
923 &self,
924 params: GetGammaEventsParams,
925 ) -> anyhow::Result<Vec<GammaEvent>> {
926 self.fetch_gamma_events_paginated(params).await
927 }
928
929 pub async fn request_instruments_by_search(
931 &self,
932 params: GetSearchParams,
933 ) -> anyhow::Result<Vec<InstrumentAny>> {
934 let response = self.inner.get_public_search(params).await?;
935 let ts_init = self.clock.get_time_ns();
936
937 let mut instruments = Vec::new();
938
939 if let Some(markets) = &response.markets {
940 instruments.extend(parse_markets_to_instruments(markets, ts_init));
941 }
942
943 if let Some(events) = &response.events {
944 let event_markets = flatten_event_markets(events.clone());
945 instruments.extend(parse_markets_to_instruments(&event_markets, ts_init));
946 }
947
948 log::debug!("Parsed {} instruments from search query", instruments.len());
949 Ok(instruments)
950 }
951
952 pub async fn request_tags(&self) -> anyhow::Result<Vec<GammaTag>> {
954 Ok(self.inner.get_gamma_tags().await?)
955 }
956
957 #[must_use]
959 pub fn inner(&self) -> &Arc<PolymarketGammaRawHttpClient> {
960 &self.inner
961 }
962}