1use serde::{Deserialize, Deserializer, Serialize};
14use serde_json::Value;
15
16use crate::core::types::{
17 AccountType, ExchangeError, ExchangeResult, Kline, OrderBook, OrderBookLevel, SymbolInfo, Ticker,
18};
19
20fn deserialize_string_or_vec<'de, D>(deserializer: D) -> Result<Option<Vec<String>>, D::Error>
28where
29 D: Deserializer<'de>,
30{
31 let value: Option<Value> = Option::deserialize(deserializer)?;
32 match value {
33 None | Some(Value::Null) => Ok(None),
34 Some(Value::Array(arr)) => {
35 let vec = arr
36 .iter()
37 .filter_map(|v| v.as_str().map(String::from))
38 .collect();
39 Ok(Some(vec))
40 }
41 Some(Value::String(s)) => match serde_json::from_str(&s) {
42 Ok(parsed) => Ok(Some(parsed)),
43 Err(_) => Ok(Some(vec![s])),
44 },
45 _ => Ok(None),
46 }
47}
48
49fn deserialize_string_to_f64<'de, D>(deserializer: D) -> Result<f64, D::Error>
51where
52 D: Deserializer<'de>,
53{
54 use serde::de::Error;
55 let v: Value = Value::deserialize(deserializer)?;
56 match v {
57 Value::Number(n) => n
58 .as_f64()
59 .ok_or_else(|| Error::custom("number out of range")),
60 Value::String(s) => {
61 let s = if s.starts_with('.') {
63 format!("0{}", s)
64 } else {
65 s
66 };
67 s.parse::<f64>()
68 .map_err(|_| Error::custom(format!("invalid float: {}", s)))
69 }
70 _ => Err(Error::custom("expected string or number")),
71 }
72}
73
74fn _deserialize_opt_string_to_f64<'de, D>(deserializer: D) -> Result<Option<f64>, D::Error>
76where
77 D: Deserializer<'de>,
78{
79 use serde::de::Error;
80 let v: Option<Value> = Option::deserialize(deserializer)?;
81 match v {
82 None | Some(Value::Null) => Ok(None),
83 Some(Value::Number(n)) => Ok(n.as_f64()),
84 Some(Value::String(s)) => {
85 let s = if s.starts_with('.') {
86 format!("0{}", s)
87 } else {
88 s
89 };
90 if s.is_empty() {
91 Ok(None)
92 } else {
93 s.parse::<f64>()
94 .map(Some)
95 .map_err(|_| Error::custom(format!("invalid float: {}", s)))
96 }
97 }
98 _ => Ok(None),
99 }
100}
101
102fn deserialize_number_or_string<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
107where
108 D: Deserializer<'de>,
109{
110 let v: Option<Value> = Option::deserialize(deserializer)?;
111 match v {
112 None | Some(Value::Null) => Ok(None),
113 Some(Value::String(s)) => Ok(Some(s)),
114 Some(Value::Number(n)) => Ok(Some(n.to_string())),
115 Some(other) => Ok(Some(other.to_string())),
116 }
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize)]
128#[serde(rename_all = "camelCase")]
129pub struct PolyMarket {
130 pub id: String,
133 #[serde(default)]
135 pub condition_id: Option<String>,
136 #[serde(default)]
138 pub question_id: Option<String>,
139 #[serde(default)]
141 pub slug: Option<String>,
142 #[serde(default)]
144 pub question: Option<String>,
145
146 #[serde(default, deserialize_with = "deserialize_string_or_vec")]
149 pub outcomes: Option<Vec<String>>,
150 #[serde(default, deserialize_with = "deserialize_string_or_vec")]
152 pub outcome_prices: Option<Vec<String>>,
153 #[serde(default, deserialize_with = "deserialize_string_or_vec")]
155 pub clob_token_ids: Option<Vec<String>>,
156
157 #[serde(default)]
159 pub last_trade_price: Option<f64>,
160 #[serde(default)]
161 pub best_bid: Option<f64>,
162 #[serde(default)]
163 pub best_ask: Option<f64>,
164 #[serde(default)]
165 pub spread: Option<f64>,
166 #[serde(default)]
167 pub one_day_price_change: Option<f64>,
168 #[serde(default)]
169 pub one_hour_price_change: Option<f64>,
170 #[serde(default)]
171 pub one_week_price_change: Option<f64>,
172
173 #[serde(default)]
175 pub volume: Option<String>,
176 #[serde(default)]
177 pub volume_num: Option<f64>,
178 #[serde(default, rename = "volume24hr")]
179 pub volume_24hr: Option<f64>,
180 #[serde(default, rename = "volume1wk")]
181 pub volume_1wk: Option<f64>,
182 #[serde(default, rename = "volume1mo")]
183 pub volume_1mo: Option<f64>,
184
185 #[serde(default)]
187 pub liquidity: Option<String>,
188 #[serde(default)]
189 pub liquidity_num: Option<f64>,
190
191 #[serde(default)]
193 pub active: Option<bool>,
194 #[serde(default)]
195 pub closed: Option<bool>,
196 #[serde(default)]
197 pub archived: Option<bool>,
198 #[serde(default)]
199 pub accepting_orders: Option<bool>,
200 #[serde(default)]
201 pub enable_order_book: Option<bool>,
202 #[serde(default)]
203 pub restricted: Option<bool>,
204
205 #[serde(default)]
207 pub start_date: Option<String>,
208 #[serde(default)]
209 pub end_date: Option<String>,
210 #[serde(default)]
211 pub created_at: Option<String>,
212 #[serde(default)]
213 pub updated_at: Option<String>,
214
215 #[serde(default)]
217 pub category: Option<String>,
218 #[serde(default)]
219 pub description: Option<String>,
220 #[serde(default)]
221 pub resolution_source: Option<String>,
222 #[serde(default)]
223 pub image: Option<String>,
224 #[serde(default)]
225 pub icon: Option<String>,
226 #[serde(default)]
227 pub market_type: Option<String>,
228
229 #[serde(default)]
231 pub order_price_min_tick_size: Option<f64>,
232 #[serde(default)]
233 pub order_min_size: Option<f64>,
234 #[serde(default)]
235 pub maker_base_fee: Option<i32>,
236 #[serde(default)]
237 pub taker_base_fee: Option<i32>,
238
239 #[serde(default)]
241 pub tags: Option<Vec<PolyTag>>,
242}
243
244impl PolyMarket {
245 pub fn yes_price(&self) -> Option<f64> {
247 self.outcome_prices
248 .as_ref()
249 .and_then(|p| p.first())
250 .and_then(|s| s.parse::<f64>().ok())
251 }
252
253 pub fn no_price(&self) -> Option<f64> {
255 self.outcome_prices
256 .as_ref()
257 .and_then(|p| p.get(1))
258 .and_then(|s| s.parse::<f64>().ok())
259 }
260
261 pub fn yes_token_id(&self) -> Option<&str> {
263 self.clob_token_ids
264 .as_ref()
265 .and_then(|ids| ids.first())
266 .map(|s| s.as_str())
267 }
268
269 pub fn no_token_id(&self) -> Option<&str> {
271 self.clob_token_ids
272 .as_ref()
273 .and_then(|ids| ids.get(1))
274 .map(|s| s.as_str())
275 }
276
277 pub fn is_tradeable(&self) -> bool {
279 self.active.unwrap_or(false)
280 && !self.closed.unwrap_or(true)
281 && self.enable_order_book.unwrap_or(false)
282 }
283}
284
285#[derive(Debug, Clone, Serialize, Deserialize)]
287pub struct PolyTag {
288 pub id: Option<String>,
289 pub label: Option<String>,
290 pub slug: Option<String>,
291}
292
293#[derive(Debug, Clone, Serialize, Deserialize)]
297#[serde(rename_all = "camelCase")]
298pub struct PolyEvent {
299 pub id: String,
300 #[serde(default)]
301 pub ticker: Option<String>,
302 #[serde(default)]
303 pub slug: Option<String>,
304 #[serde(default)]
305 pub title: Option<String>,
306 #[serde(default)]
307 pub subtitle: Option<String>,
308 #[serde(default)]
309 pub description: Option<String>,
310 #[serde(default)]
311 pub active: Option<bool>,
312 #[serde(default)]
313 pub closed: Option<bool>,
314 #[serde(default)]
315 pub archived: Option<bool>,
316 #[serde(default)]
317 pub start_date: Option<String>,
318 #[serde(default)]
319 pub end_date: Option<String>,
320 #[serde(default)]
321 pub liquidity: Option<f64>,
322 #[serde(default)]
323 pub volume: Option<f64>,
324 #[serde(default, rename = "volume24hr")]
325 pub volume_24hr: Option<f64>,
326 #[serde(default)]
327 pub category: Option<String>,
328 #[serde(default)]
329 pub image: Option<String>,
330 #[serde(default)]
331 pub icon: Option<String>,
332 #[serde(default)]
333 pub markets: Option<Vec<PolyMarket>>,
334}
335
336#[derive(Debug, Clone, Serialize, Deserialize)]
344pub struct ClobMarket {
345 pub condition_id: String,
347 #[serde(default)]
349 pub question: Option<String>,
350 #[serde(default)]
352 pub market_slug: Option<String>,
353 #[serde(default)]
355 pub active: Option<bool>,
356 #[serde(default)]
358 pub closed: Option<bool>,
359 #[serde(rename = "end_date_iso", default)]
361 pub end_date: Option<String>,
362 #[serde(default)]
364 pub tokens: Vec<PolyToken>,
365 #[serde(default, deserialize_with = "deserialize_number_or_string")]
367 pub minimum_order_size: Option<String>,
368 #[serde(default, deserialize_with = "deserialize_number_or_string")]
370 pub minimum_tick_size: Option<String>,
371 #[serde(default)]
373 pub description: Option<String>,
374 #[serde(default)]
376 pub maker_base_fee: Option<i32>,
377 #[serde(default)]
379 pub taker_base_fee: Option<i32>,
380 #[serde(default)]
382 pub neg_risk: Option<bool>,
383}
384
385#[derive(Debug, Clone, Serialize, Deserialize)]
387pub struct PolyToken {
388 pub token_id: String,
390 pub outcome: String,
392 #[serde(default)]
394 pub price: Option<f64>,
395 #[serde(default)]
397 pub winner: Option<bool>,
398}
399
400#[derive(Debug, Clone, Serialize, Deserialize)]
404pub struct PolyOrderBook {
405 pub market: String,
407 pub asset_id: String,
409 #[serde(default)]
411 pub timestamp: Option<String>,
412 #[serde(default)]
414 pub bids: Vec<PolyPriceLevel>,
415 #[serde(default)]
417 pub asks: Vec<PolyPriceLevel>,
418 #[serde(default)]
420 pub min_order_size: Option<String>,
421 #[serde(default)]
423 pub tick_size: Option<String>,
424}
425
426#[derive(Debug, Clone, Serialize, Deserialize)]
428pub struct PolyPriceLevel {
429 pub price: String,
431 pub size: String,
433}
434
435impl PolyPriceLevel {
436 pub fn price_f64(&self) -> Option<f64> {
438 let s = if self.price.starts_with('.') {
439 format!("0{}", self.price)
440 } else {
441 self.price.clone()
442 };
443 s.parse::<f64>().ok()
444 }
445
446 pub fn size_f64(&self) -> Option<f64> {
448 self.size.parse::<f64>().ok()
449 }
450}
451
452#[derive(Debug, Clone, Serialize, Deserialize)]
456pub struct PriceHistoryPoint {
457 #[serde(rename = "t")]
459 pub timestamp: i64,
460 #[serde(rename = "p")]
462 pub price: f64,
463}
464
465#[derive(Debug, Clone, Serialize, Deserialize)]
467pub struct PolyMidpoint {
468 #[serde(deserialize_with = "deserialize_string_to_f64")]
469 pub mid: f64,
470}
471
472#[derive(Debug, Clone, Serialize, Deserialize)]
474pub struct PolyOrder {
475 pub id: String,
476 pub status: String,
477 pub market: String,
478 pub asset_id: String,
479 pub side: String,
480 pub original_size: String,
481 pub size_matched: String,
482 pub price: String,
483 pub outcome: String,
484 pub owner: String,
485 #[serde(default)]
486 pub maker_address: Option<String>,
487 #[serde(default)]
488 pub created_at: Option<String>,
489 #[serde(default)]
490 pub expiration: Option<String>,
491 #[serde(default)]
492 pub order_type: Option<String>,
493}
494
495#[derive(Debug, Clone, Serialize, Deserialize)]
497pub struct PolyTrade {
498 pub id: String,
499 pub market: String,
500 pub asset_id: String,
501 pub side: String,
502 pub size: String,
503 pub price: String,
504 #[serde(default)]
505 pub status: Option<String>,
506 #[serde(default)]
507 pub outcome: Option<String>,
508 #[serde(default)]
509 pub match_time: Option<String>,
510 #[serde(default)]
511 pub transaction_hash: Option<String>,
512}
513
514#[derive(Debug, Clone, Serialize, Deserialize)]
520pub struct WsSubscription {
521 #[serde(rename = "type")]
522 pub msg_type: String,
523 #[serde(skip_serializing_if = "Option::is_none")]
524 pub assets_ids: Option<Vec<String>>,
525 #[serde(skip_serializing_if = "Option::is_none")]
526 pub operation: Option<String>,
527}
528
529#[derive(Debug, Clone, Serialize, Deserialize)]
531pub struct WsBookSnapshot {
532 pub event_type: String,
533 #[serde(default)]
534 pub asset_id: Option<String>,
535 pub market: String,
536 pub bids: Vec<PolyPriceLevel>,
537 pub asks: Vec<PolyPriceLevel>,
538 #[serde(default)]
539 pub timestamp: Option<String>,
540 #[serde(default)]
541 pub hash: Option<String>,
542}
543
544#[derive(Debug, Clone, Serialize, Deserialize)]
549pub struct WsPriceChange {
550 pub event_type: String,
551 #[serde(default)]
552 pub asset_id: Option<String>,
553 #[serde(default)]
554 pub market: Option<String>,
555 #[serde(default)]
557 pub changes: Vec<PolyPriceLevel>,
558 #[serde(default)]
560 pub price: Option<String>,
561 #[serde(default)]
563 pub size: Option<String>,
564 #[serde(default)]
565 pub side: Option<String>,
566 #[serde(default)]
567 pub timestamp: Option<String>,
568}
569
570#[derive(Debug, Clone, Serialize, Deserialize)]
572pub struct WsLastTradePrice {
573 pub event_type: String,
574 #[serde(default)]
575 pub asset_id: Option<String>,
576 pub market: String,
577 pub price: String,
578 #[serde(default)]
579 pub size: Option<String>,
580 #[serde(default)]
581 pub side: Option<String>,
582 #[serde(default)]
583 pub timestamp: Option<String>,
584}
585
586#[derive(Debug, Clone, Serialize, Deserialize)]
588pub struct WsTickSizeChange {
589 pub event_type: String,
590 #[serde(default)]
591 pub asset_id: Option<String>,
592 pub market: String,
593 pub old_tick_size: String,
594 pub new_tick_size: String,
595 #[serde(default)]
596 pub side: Option<String>,
597 #[serde(default)]
598 pub timestamp: Option<String>,
599}
600
601#[derive(Debug, Clone, Serialize, Deserialize)]
603pub struct WsBestBidAsk {
604 pub event_type: String,
605 #[serde(default)]
606 pub asset_id: Option<String>,
607 pub market: String,
608 pub best_bid: String,
609 pub best_ask: String,
610 #[serde(default)]
611 pub spread: Option<String>,
612 #[serde(default)]
613 pub timestamp: Option<String>,
614}
615
616pub struct PolymarketParser;
622
623impl PolymarketParser {
624 pub fn parse_clob_markets(response: &Value) -> ExchangeResult<Vec<ClobMarket>> {
632 let arr = response
633 .get("data")
634 .and_then(|v| v.as_array())
635 .or_else(|| response.as_array())
636 .ok_or_else(|| ExchangeError::Parse("Expected array of markets".to_string()))?;
637
638 arr.iter()
639 .map(|v| {
640 serde_json::from_value(v.clone())
641 .map_err(|e| ExchangeError::Parse(format!("Failed to parse ClobMarket: {}", e)))
642 })
643 .collect()
644 }
645
646 pub fn parse_clob_market(response: &Value) -> ExchangeResult<ClobMarket> {
648 serde_json::from_value(response.clone())
649 .map_err(|e| ExchangeError::Parse(format!("Failed to parse ClobMarket: {}", e)))
650 }
651
652 pub fn parse_gamma_markets(response: &Value) -> ExchangeResult<Vec<PolyMarket>> {
654 let arr = response
655 .as_array()
656 .or_else(|| response.get("data").and_then(|v| v.as_array()))
657 .ok_or_else(|| ExchangeError::Parse("Expected array of markets".to_string()))?;
658
659 arr.iter()
660 .map(|v| {
661 serde_json::from_value(v.clone())
662 .map_err(|e| ExchangeError::Parse(format!("Failed to parse PolyMarket: {}", e)))
663 })
664 .collect()
665 }
666
667 pub fn parse_gamma_market(response: &Value) -> ExchangeResult<PolyMarket> {
669 serde_json::from_value(response.clone())
670 .map_err(|e| ExchangeError::Parse(format!("Failed to parse PolyMarket: {}", e)))
671 }
672
673 pub fn parse_events(response: &Value) -> ExchangeResult<Vec<PolyEvent>> {
675 let arr = response
676 .as_array()
677 .or_else(|| response.get("data").and_then(|v| v.as_array()))
678 .ok_or_else(|| ExchangeError::Parse("Expected array of events".to_string()))?;
679
680 arr.iter()
681 .map(|v| {
682 serde_json::from_value(v.clone())
683 .map_err(|e| ExchangeError::Parse(format!("Failed to parse PolyEvent: {}", e)))
684 })
685 .collect()
686 }
687
688 pub fn parse_event(response: &Value) -> ExchangeResult<PolyEvent> {
690 serde_json::from_value(response.clone())
691 .map_err(|e| ExchangeError::Parse(format!("Failed to parse PolyEvent: {}", e)))
692 }
693
694 pub fn parse_order_book(response: &Value) -> ExchangeResult<PolyOrderBook> {
700 serde_json::from_value(response.clone())
701 .map_err(|e| ExchangeError::Parse(format!("Failed to parse PolyOrderBook: {}", e)))
702 }
703
704 pub fn parse_midpoint(response: &Value) -> ExchangeResult<PolyMidpoint> {
706 serde_json::from_value(response.clone())
707 .map_err(|e| ExchangeError::Parse(format!("Failed to parse PolyMidpoint: {}", e)))
708 }
709
710 pub fn parse_price(response: &Value) -> ExchangeResult<f64> {
712 let price_str = response
713 .get("price")
714 .and_then(|v| v.as_str())
715 .ok_or_else(|| ExchangeError::Parse("Missing 'price' field".to_string()))?;
716
717 let normalized = if price_str.starts_with('.') {
718 format!("0{}", price_str)
719 } else {
720 price_str.to_string()
721 };
722
723 normalized
724 .parse::<f64>()
725 .map_err(|e| ExchangeError::Parse(format!("Invalid price '{}': {}", price_str, e)))
726 }
727
728 pub fn parse_price_history(response: &Value) -> ExchangeResult<Vec<PriceHistoryPoint>> {
733 let arr = response
734 .get("history")
735 .and_then(|v| v.as_array())
736 .or_else(|| response.as_array())
737 .ok_or_else(|| ExchangeError::Parse("Expected price history array".to_string()))?;
738
739 arr.iter()
740 .map(|v| {
741 serde_json::from_value(v.clone()).map_err(|e| {
742 ExchangeError::Parse(format!("Failed to parse PriceHistoryPoint: {}", e))
743 })
744 })
745 .collect()
746 }
747
748 pub fn get_next_cursor(response: &Value) -> Option<String> {
750 response
751 .get("next_cursor")
752 .and_then(|v| v.as_str())
753 .filter(|s| !s.is_empty() && *s != "LTE=")
754 .map(String::from)
755 }
756
757 pub fn check_error(response: &Value) -> ExchangeResult<()> {
759 if let Some(error) = response.get("error") {
760 let msg = error
761 .as_str()
762 .unwrap_or("Unknown API error")
763 .to_string();
764 return Err(ExchangeError::Api { code: 0, message: msg });
765 }
766 Ok(())
767 }
768}
769
770pub fn clob_market_to_symbol_info(market: &ClobMarket, account_type: AccountType) -> SymbolInfo {
779 let question_short = market
780 .question
781 .as_deref()
782 .unwrap_or("Unknown")
783 .chars()
784 .take(50)
785 .collect::<String>();
786
787 SymbolInfo {
788 symbol: market.condition_id.clone(),
789 base_asset: question_short,
790 quote_asset: "USDC".to_string(),
791 status: if market.active.unwrap_or(false) && !market.closed.unwrap_or(true) {
792 "TRADING"
793 } else {
794 "BREAK"
795 }
796 .to_string(),
797 price_precision: 4,
798 quantity_precision: 2,
799 min_quantity: market
800 .minimum_order_size
801 .as_ref()
802 .and_then(|s| s.parse::<f64>().ok()),
803 max_quantity: None,
804 tick_size: market
806 .minimum_tick_size
807 .as_ref()
808 .and_then(|s| s.parse::<f64>().ok()),
809 step_size: market
810 .minimum_tick_size
811 .as_ref()
812 .and_then(|s| s.parse::<f64>().ok()),
813 min_notional: None,
814 account_type,
815 }
816}
817
818pub fn poly_market_to_symbol_info(market: &PolyMarket, account_type: AccountType) -> SymbolInfo {
820 let condition_id = market
821 .condition_id
822 .as_deref()
823 .unwrap_or(&market.id)
824 .to_string();
825
826 let question = market
827 .question
828 .as_deref()
829 .unwrap_or("Unknown")
830 .chars()
831 .take(50)
832 .collect::<String>();
833
834 SymbolInfo {
835 symbol: condition_id,
836 base_asset: question,
837 quote_asset: "USDC".to_string(),
838 status: if market.active.unwrap_or(false) && !market.closed.unwrap_or(true) {
839 "TRADING"
840 } else {
841 "BREAK"
842 }
843 .to_string(),
844 price_precision: 4,
845 quantity_precision: 2,
846 min_quantity: market.order_min_size,
847 max_quantity: None,
848 tick_size: market.order_price_min_tick_size,
850 step_size: market.order_price_min_tick_size,
851 min_notional: None,
852 account_type,
853 }
854}
855
856pub fn price_history_to_klines(
863 history: Vec<PriceHistoryPoint>,
864 interval_ms: u64,
865) -> Vec<Kline> {
866 history
867 .into_iter()
868 .map(|point| {
869 let open_time = point.timestamp * 1000; let price = point.price;
871
872 Kline {
873 open_time,
874 open: price,
875 high: price,
876 low: price,
877 close: price,
878 volume: 0.0,
879 quote_volume: None,
880 close_time: Some(open_time + interval_ms as i64 - 1),
881 trades: None,
882 }
883 })
884 .collect()
885}
886
887pub fn poly_orderbook_to_v5(book: &PolyOrderBook) -> OrderBook {
893 let mut bids: Vec<OrderBookLevel> = book
894 .bids
895 .iter()
896 .filter_map(|level| {
897 let p = level.price_f64()?;
898 let s = level.size_f64()?;
899 Some(OrderBookLevel::new(p, s))
900 })
901 .collect();
902 bids.sort_by(|a, b| b.price.partial_cmp(&a.price).unwrap_or(std::cmp::Ordering::Equal));
904
905 let mut asks: Vec<OrderBookLevel> = book
906 .asks
907 .iter()
908 .filter_map(|level| {
909 let p = level.price_f64()?;
910 let s = level.size_f64()?;
911 Some(OrderBookLevel::new(p, s))
912 })
913 .collect();
914 asks.sort_by(|a, b| a.price.partial_cmp(&b.price).unwrap_or(std::cmp::Ordering::Equal));
916
917 OrderBook {
918 bids,
919 asks,
920 timestamp: chrono::Utc::now().timestamp_millis(),
921 sequence: book.timestamp.clone(),
922 last_update_id: None,
923 first_update_id: None,
924 prev_update_id: None,
925 event_time: None,
926 transaction_time: None,
927 checksum: None,
928 }
929}
930
931pub fn clob_market_to_ticker(market: &ClobMarket) -> Option<Ticker> {
935 let yes_token = market
936 .tokens
937 .iter()
938 .find(|t| t.outcome == "Yes")
939 .or_else(|| market.tokens.first())?;
940 let price = yes_token.price?;
941
942 Some(Ticker {
943 last_price: price,
944 bid_price: None,
945 ask_price: None,
946 high_24h: None,
947 low_24h: None,
948 volume_24h: None,
949 quote_volume_24h: None,
950 price_change_24h: None,
951 price_change_percent_24h: None,
952 timestamp: chrono::Utc::now().timestamp_millis(),
953 })
954}
955
956pub fn poly_market_to_ticker(market: &PolyMarket) -> Ticker {
958 let last_price = market
959 .last_trade_price
960 .or_else(|| market.yes_price())
961 .unwrap_or(0.0);
962
963 Ticker {
964 last_price,
965 bid_price: market.best_bid,
966 ask_price: market.best_ask,
967 high_24h: None,
968 low_24h: None,
969 volume_24h: market.volume_24hr,
970 quote_volume_24h: market.volume_24hr,
971 price_change_24h: market.one_day_price_change,
972 price_change_percent_24h: market
973 .one_day_price_change
974 .zip(Some(last_price))
975 .map(|(change, _)| change * 100.0),
976 timestamp: chrono::Utc::now().timestamp_millis(),
977 }
978}
979
980pub fn interval_to_ms(interval: &str) -> u64 {
982 match interval {
983 "1m" => 60_000,
984 "1h" => 3_600_000,
985 "6h" => 21_600_000,
986 "1d" => 86_400_000,
987 "1w" => 604_800_000,
988 _ => 86_400_000,
989 }
990}