1use serde::Deserialize;
8use std::collections::HashMap;
9
10#[derive(Debug, Clone, Deserialize)]
12pub struct SymbolConfig {
13 pub template: String,
16
17 pub default_quote: String,
19
20 #[serde(default)]
22 pub case: SymbolCase,
23}
24
25#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq, Default)]
27#[serde(rename_all = "lowercase")]
28pub enum SymbolCase {
29 #[default]
30 Upper,
31 Lower,
32}
33
34#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq, Default)]
36#[serde(rename_all = "UPPERCASE")]
37pub enum HttpMethod {
38 #[default]
39 #[serde(alias = "get")]
40 GET,
41 #[serde(alias = "post")]
42 POST,
43}
44
45#[derive(Debug, Clone, Deserialize)]
47pub struct EndpointDescriptor {
48 #[serde(default)]
50 pub method: HttpMethod,
51
52 pub path: String,
54
55 #[serde(default)]
58 pub params: HashMap<String, String>,
59
60 pub request_body: Option<serde_json::Value>,
62
63 pub response_root: Option<String>,
71
72 pub response: ResponseMapping,
74}
75
76#[derive(Debug, Clone, Deserialize, Default)]
81pub struct ResponseMapping {
82 pub asks_key: Option<String>,
85 pub bids_key: Option<String>,
87 pub level_format: Option<String>,
90 pub level_price_field: Option<String>,
92 pub level_size_field: Option<String>,
94
95 pub last_price: Option<String>,
97 pub high_24h: Option<String>,
98 pub low_24h: Option<String>,
99 pub volume_24h: Option<String>,
100 pub quote_volume_24h: Option<String>,
101 pub best_bid: Option<String>,
102 pub best_ask: Option<String>,
103
104 pub items_key: Option<String>,
107
108 pub filter: Option<FilterConfig>,
110
111 pub price: Option<String>,
113 pub quantity: Option<String>,
114 pub quote_quantity: Option<String>,
115 pub timestamp_ms: Option<String>,
116 pub id: Option<String>,
117 pub side: Option<SideMapping>,
118
119 pub ohlc_format: Option<String>,
124 pub ohlc_fields: Option<Vec<String>>,
127 pub open_time: Option<String>,
130 pub open: Option<String>,
131 pub high: Option<String>,
132 pub low: Option<String>,
133 pub close: Option<String>,
134 pub ohlc_volume: Option<String>,
136 pub close_time: Option<String>,
137}
138
139#[derive(Debug, Clone, Deserialize)]
141pub struct SideMapping {
142 pub field: String,
144 pub mapping: HashMap<String, String>,
146}
147
148#[derive(Debug, Clone, Deserialize)]
150pub struct FilterConfig {
151 pub field: String,
153 pub value: String,
155}
156
157#[derive(Debug, Clone, Deserialize, Default)]
160pub struct CapabilitySet {
161 pub order_book: Option<EndpointDescriptor>,
163 pub ticker: Option<EndpointDescriptor>,
165 pub trades: Option<EndpointDescriptor>,
167 pub ohlc: Option<EndpointDescriptor>,
169}
170
171#[derive(Debug, Clone, Deserialize)]
177pub struct VenueDescriptor {
178 pub id: String,
180 pub name: String,
182 pub base_url: String,
184 pub timeout_secs: Option<u64>,
186 pub rate_limit_per_sec: Option<u32>,
188 pub symbol: SymbolConfig,
190 #[serde(default)]
192 pub headers: HashMap<String, String>,
193 #[serde(default)]
195 pub capabilities: CapabilitySet,
196}
197
198impl VenueDescriptor {
199 pub fn format_pair(&self, base: &str, quote: Option<&str>) -> String {
203 let q = quote.unwrap_or(&self.symbol.default_quote);
204 let raw = self
205 .symbol
206 .template
207 .replace("{base}", base)
208 .replace("{quote}", q);
209 match self.symbol.case {
210 SymbolCase::Upper => raw.to_uppercase(),
211 SymbolCase::Lower => raw.to_lowercase(),
212 }
213 }
214
215 pub fn has_order_book(&self) -> bool {
217 self.capabilities.order_book.is_some()
218 }
219 pub fn has_ticker(&self) -> bool {
220 self.capabilities.ticker.is_some()
221 }
222 pub fn has_trades(&self) -> bool {
223 self.capabilities.trades.is_some()
224 }
225 pub fn has_ohlc(&self) -> bool {
226 self.capabilities.ohlc.is_some()
227 }
228
229 pub fn capability_names(&self) -> Vec<&'static str> {
231 let mut caps = Vec::new();
232 if self.has_order_book() {
233 caps.push("order_book");
234 }
235 if self.has_ticker() {
236 caps.push("ticker");
237 }
238 if self.has_trades() {
239 caps.push("trades");
240 }
241 if self.has_ohlc() {
242 caps.push("ohlc");
243 }
244 caps
245 }
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251
252 #[test]
253 fn test_symbol_case_default_is_upper() {
254 let case = SymbolCase::default();
255 assert_eq!(case, SymbolCase::Upper);
256 }
257
258 #[test]
259 fn test_http_method_default_is_get() {
260 let method = HttpMethod::default();
261 assert_eq!(method, HttpMethod::GET);
262 }
263
264 #[test]
265 fn test_format_pair_upper() {
266 let desc = VenueDescriptor {
267 id: "test".to_string(),
268 name: "Test".to_string(),
269 base_url: "https://example.com".to_string(),
270 timeout_secs: None,
271 rate_limit_per_sec: None,
272 symbol: SymbolConfig {
273 template: "{base}{quote}".to_string(),
274 default_quote: "USDT".to_string(),
275 case: SymbolCase::Upper,
276 },
277 headers: HashMap::new(),
278 capabilities: CapabilitySet::default(),
279 };
280 assert_eq!(desc.format_pair("BTC", None), "BTCUSDT");
281 assert_eq!(desc.format_pair("btc", None), "BTCUSDT");
282 assert_eq!(desc.format_pair("ETH", Some("USD")), "ETHUSD");
283 }
284
285 #[test]
286 fn test_format_pair_lower() {
287 let desc = VenueDescriptor {
288 id: "htx".to_string(),
289 name: "HTX".to_string(),
290 base_url: "https://api.huobi.pro".to_string(),
291 timeout_secs: None,
292 rate_limit_per_sec: None,
293 symbol: SymbolConfig {
294 template: "{base}{quote}".to_string(),
295 default_quote: "USDT".to_string(),
296 case: SymbolCase::Lower,
297 },
298 headers: HashMap::new(),
299 capabilities: CapabilitySet::default(),
300 };
301 assert_eq!(desc.format_pair("BTC", None), "btcusdt");
302 }
303
304 #[test]
305 fn test_format_pair_underscore() {
306 let desc = VenueDescriptor {
307 id: "biconomy".to_string(),
308 name: "Biconomy".to_string(),
309 base_url: "https://api.biconomy.com".to_string(),
310 timeout_secs: None,
311 rate_limit_per_sec: None,
312 symbol: SymbolConfig {
313 template: "{base}_{quote}".to_string(),
314 default_quote: "USDT".to_string(),
315 case: SymbolCase::Upper,
316 },
317 headers: HashMap::new(),
318 capabilities: CapabilitySet::default(),
319 };
320 assert_eq!(desc.format_pair("PUSD", None), "PUSD_USDT");
321 }
322
323 #[test]
324 fn test_format_pair_dash() {
325 let desc = VenueDescriptor {
326 id: "okx".to_string(),
327 name: "OKX".to_string(),
328 base_url: "https://www.okx.com".to_string(),
329 timeout_secs: None,
330 rate_limit_per_sec: None,
331 symbol: SymbolConfig {
332 template: "{base}-{quote}".to_string(),
333 default_quote: "USDT".to_string(),
334 case: SymbolCase::Upper,
335 },
336 headers: HashMap::new(),
337 capabilities: CapabilitySet::default(),
338 };
339 assert_eq!(desc.format_pair("BTC", None), "BTC-USDT");
340 }
341
342 #[test]
343 fn test_capability_names_all() {
344 let desc = VenueDescriptor {
345 id: "test".to_string(),
346 name: "Test".to_string(),
347 base_url: "https://example.com".to_string(),
348 timeout_secs: None,
349 rate_limit_per_sec: None,
350 symbol: SymbolConfig {
351 template: "{base}{quote}".to_string(),
352 default_quote: "USDT".to_string(),
353 case: SymbolCase::Upper,
354 },
355 headers: HashMap::new(),
356 capabilities: CapabilitySet {
357 order_book: Some(EndpointDescriptor {
358 method: HttpMethod::GET,
359 path: "/depth".to_string(),
360 params: HashMap::new(),
361 request_body: None,
362 response_root: None,
363 response: ResponseMapping::default(),
364 }),
365 ticker: Some(EndpointDescriptor {
366 method: HttpMethod::GET,
367 path: "/ticker".to_string(),
368 params: HashMap::new(),
369 request_body: None,
370 response_root: None,
371 response: ResponseMapping::default(),
372 }),
373 trades: Some(EndpointDescriptor {
374 method: HttpMethod::GET,
375 path: "/trades".to_string(),
376 params: HashMap::new(),
377 request_body: None,
378 response_root: None,
379 response: ResponseMapping::default(),
380 }),
381 ohlc: None,
382 },
383 };
384 let caps = desc.capability_names();
385 assert_eq!(caps, vec!["order_book", "ticker", "trades"]);
386 }
387
388 #[test]
389 fn test_capability_names_partial() {
390 let desc = VenueDescriptor {
391 id: "test".to_string(),
392 name: "Test".to_string(),
393 base_url: "https://example.com".to_string(),
394 timeout_secs: None,
395 rate_limit_per_sec: None,
396 symbol: SymbolConfig {
397 template: "{base}{quote}".to_string(),
398 default_quote: "USDT".to_string(),
399 case: SymbolCase::Upper,
400 },
401 headers: HashMap::new(),
402 capabilities: CapabilitySet {
403 order_book: Some(EndpointDescriptor {
404 method: HttpMethod::GET,
405 path: "/depth".to_string(),
406 params: HashMap::new(),
407 request_body: None,
408 response_root: None,
409 response: ResponseMapping::default(),
410 }),
411 ticker: None,
412 trades: None,
413 ohlc: None,
414 },
415 };
416 assert_eq!(desc.capability_names(), vec!["order_book"]);
417 }
418
419 #[test]
420 fn test_deserialize_binance_yaml() {
421 let yaml = r#"
422id: binance
423name: Binance Spot
424base_url: https://api.binance.com
425timeout_secs: 15
426rate_limit_per_sec: 10
427
428symbol:
429 template: "{base}{quote}"
430 default_quote: USDT
431
432capabilities:
433 order_book:
434 path: /api/v3/depth
435 params:
436 symbol: "{pair}"
437 limit: "100"
438 response:
439 asks_key: asks
440 bids_key: bids
441 level_format: positional
442
443 ticker:
444 path: /api/v3/ticker/24hr
445 params:
446 symbol: "{pair}"
447 response:
448 last_price: lastPrice
449 high_24h: highPrice
450 low_24h: lowPrice
451 volume_24h: volume
452 quote_volume_24h: quoteVolume
453 best_bid: bidPrice
454 best_ask: askPrice
455
456 trades:
457 path: /api/v3/trades
458 params:
459 symbol: "{pair}"
460 limit: "{limit}"
461 response:
462 price: price
463 quantity: qty
464 quote_quantity: quoteQty
465 timestamp_ms: time
466 id: id
467 side:
468 field: isBuyerMaker
469 mapping:
470 "true": sell
471 "false": buy
472"#;
473 let desc: VenueDescriptor = serde_yaml::from_str(yaml).unwrap();
474 assert_eq!(desc.id, "binance");
475 assert_eq!(desc.name, "Binance Spot");
476 assert_eq!(desc.base_url, "https://api.binance.com");
477 assert_eq!(desc.timeout_secs, Some(15));
478 assert_eq!(desc.symbol.template, "{base}{quote}");
479 assert_eq!(desc.symbol.default_quote, "USDT");
480 assert_eq!(desc.symbol.case, SymbolCase::Upper);
481
482 let ob = desc.capabilities.order_book.as_ref().unwrap();
484 assert_eq!(ob.path, "/api/v3/depth");
485 assert_eq!(ob.params.get("symbol"), Some(&"{pair}".to_string()));
486 assert_eq!(ob.response.asks_key, Some("asks".to_string()));
487 assert_eq!(ob.response.level_format, Some("positional".to_string()));
488
489 let ticker = desc.capabilities.ticker.as_ref().unwrap();
491 assert_eq!(ticker.response.last_price, Some("lastPrice".to_string()));
492 assert_eq!(ticker.response.volume_24h, Some("volume".to_string()));
493
494 let trades = desc.capabilities.trades.as_ref().unwrap();
496 assert_eq!(trades.response.price, Some("price".to_string()));
497 let side = trades.response.side.as_ref().unwrap();
498 assert_eq!(side.field, "isBuyerMaker");
499 assert_eq!(side.mapping.get("true"), Some(&"sell".to_string()));
500 }
501
502 #[test]
503 fn test_deserialize_htx_lowercase() {
504 let yaml = r#"
505id: htx
506name: HTX
507base_url: https://api.huobi.pro
508
509symbol:
510 template: "{base}{quote}"
511 default_quote: USDT
512 case: lower
513
514capabilities:
515 order_book:
516 path: /market/depth
517 params:
518 symbol: "{pair}"
519 type: step0
520 response_root: tick
521 response:
522 asks_key: asks
523 bids_key: bids
524 level_format: positional
525"#;
526 let desc: VenueDescriptor = serde_yaml::from_str(yaml).unwrap();
527 assert_eq!(desc.symbol.case, SymbolCase::Lower);
528 assert_eq!(desc.format_pair("BTC", None), "btcusdt");
529 let ob = desc.capabilities.order_book.as_ref().unwrap();
530 assert_eq!(ob.response_root, Some("tick".to_string()));
531 }
532
533 #[test]
534 fn test_deserialize_post_method() {
535 let yaml = r#"
536id: crypto_com
537name: Crypto.com
538base_url: https://api.crypto.com/exchange/v1
539
540symbol:
541 template: "{base}_{quote}"
542 default_quote: USDT
543
544capabilities:
545 order_book:
546 method: POST
547 path: /public/get-book
548 request_body:
549 method: "public/get-book"
550 params:
551 instrument_name: "{pair}"
552 depth: "100"
553 response_root: "result.data.0"
554 response:
555 asks_key: asks
556 bids_key: bids
557 level_format: positional
558"#;
559 let desc: VenueDescriptor = serde_yaml::from_str(yaml).unwrap();
560 let ob = desc.capabilities.order_book.as_ref().unwrap();
561 assert_eq!(ob.method, HttpMethod::POST);
562 assert!(ob.request_body.is_some());
563 assert_eq!(ob.response_root, Some("result.data.0".to_string()));
564 }
565
566 #[test]
567 fn test_deserialize_object_level_format() {
568 let yaml = r#"
569id: coinbase
570name: Coinbase
571base_url: https://api.coinbase.com
572
573symbol:
574 template: "{base}-{quote}"
575 default_quote: USD
576
577capabilities:
578 order_book:
579 path: /api/v3/brokerage/market/product_book
580 params:
581 product_id: "{pair}"
582 limit: "100"
583 response_root: pricebook
584 response:
585 asks_key: asks
586 bids_key: bids
587 level_format: object
588 level_price_field: price
589 level_size_field: size
590"#;
591 let desc: VenueDescriptor = serde_yaml::from_str(yaml).unwrap();
592 let ob = desc.capabilities.order_book.as_ref().unwrap();
593 assert_eq!(ob.response.level_format, Some("object".to_string()));
594 assert_eq!(ob.response.level_price_field, Some("price".to_string()));
595 assert_eq!(ob.response.level_size_field, Some("size".to_string()));
596 assert_eq!(ob.response_root, Some("pricebook".to_string()));
597 }
598
599 #[test]
600 fn test_venue_descriptor_format_pair_upper() {
601 let desc = VenueDescriptor {
602 id: "test".to_string(),
603 name: "Test".to_string(),
604 base_url: "https://example.com".to_string(),
605 timeout_secs: None,
606 rate_limit_per_sec: None,
607 symbol: SymbolConfig {
608 template: "{base}{quote}".to_string(),
609 default_quote: "USDT".to_string(),
610 case: SymbolCase::Upper,
611 },
612 headers: HashMap::new(),
613 capabilities: CapabilitySet::default(),
614 };
615 assert_eq!(desc.format_pair("btc", None), "BTCUSDT");
616 assert_eq!(desc.format_pair("ETH", Some("USD")), "ETHUSD");
617 }
618
619 #[test]
620 fn test_venue_descriptor_format_pair_lower() {
621 let desc = VenueDescriptor {
622 id: "test".to_string(),
623 name: "Test".to_string(),
624 base_url: "https://example.com".to_string(),
625 timeout_secs: None,
626 rate_limit_per_sec: None,
627 symbol: SymbolConfig {
628 template: "{base}{quote}".to_string(),
629 default_quote: "USDT".to_string(),
630 case: SymbolCase::Lower,
631 },
632 headers: HashMap::new(),
633 capabilities: CapabilitySet::default(),
634 };
635 assert_eq!(desc.format_pair("BTC", None), "btcusdt");
636 assert_eq!(desc.format_pair("ETH", Some("usd")), "ethusd");
637 }
638
639 #[test]
640 fn test_venue_descriptor_format_pair_with_separator() {
641 let desc = VenueDescriptor {
642 id: "test".to_string(),
643 name: "Test".to_string(),
644 base_url: "https://example.com".to_string(),
645 timeout_secs: None,
646 rate_limit_per_sec: None,
647 symbol: SymbolConfig {
648 template: "{base}_{quote}".to_string(),
649 default_quote: "USDT".to_string(),
650 case: SymbolCase::Upper,
651 },
652 headers: HashMap::new(),
653 capabilities: CapabilitySet::default(),
654 };
655 assert_eq!(desc.format_pair("BTC", None), "BTC_USDT");
656 assert_eq!(desc.format_pair("PUSD", Some("USD")), "PUSD_USD");
657 }
658
659 #[test]
660 fn test_capability_names() {
661 let desc = VenueDescriptor {
663 id: "empty".to_string(),
664 name: "Empty".to_string(),
665 base_url: "https://example.com".to_string(),
666 timeout_secs: None,
667 rate_limit_per_sec: None,
668 symbol: SymbolConfig {
669 template: "{base}{quote}".to_string(),
670 default_quote: "USDT".to_string(),
671 case: SymbolCase::Upper,
672 },
673 headers: HashMap::new(),
674 capabilities: CapabilitySet::default(),
675 };
676 assert!(desc.capability_names().is_empty());
677
678 let desc_trades = VenueDescriptor {
680 id: "trades_only".to_string(),
681 name: "Trades Only".to_string(),
682 base_url: "https://example.com".to_string(),
683 timeout_secs: None,
684 rate_limit_per_sec: None,
685 symbol: SymbolConfig {
686 template: "{base}{quote}".to_string(),
687 default_quote: "USDT".to_string(),
688 case: SymbolCase::Upper,
689 },
690 headers: HashMap::new(),
691 capabilities: CapabilitySet {
692 order_book: None,
693 ticker: None,
694 trades: Some(EndpointDescriptor {
695 method: HttpMethod::GET,
696 path: "/trades".to_string(),
697 params: HashMap::new(),
698 request_body: None,
699 response_root: None,
700 response: ResponseMapping::default(),
701 }),
702 ohlc: None,
703 },
704 };
705 assert_eq!(desc_trades.capability_names(), vec!["trades"]);
706 }
707
708 #[test]
709 fn test_has_order_book() {
710 let with_ob = VenueDescriptor {
711 id: "x".to_string(),
712 name: "X".to_string(),
713 base_url: "https://x.com".to_string(),
714 timeout_secs: None,
715 rate_limit_per_sec: None,
716 symbol: SymbolConfig {
717 template: "{base}{quote}".to_string(),
718 default_quote: "USDT".to_string(),
719 case: SymbolCase::Upper,
720 },
721 headers: HashMap::new(),
722 capabilities: CapabilitySet {
723 order_book: Some(EndpointDescriptor {
724 method: HttpMethod::GET,
725 path: "/depth".to_string(),
726 params: HashMap::new(),
727 request_body: None,
728 response_root: None,
729 response: ResponseMapping::default(),
730 }),
731 ticker: None,
732 trades: None,
733 ohlc: None,
734 },
735 };
736 assert!(with_ob.has_order_book());
737
738 let without = VenueDescriptor {
739 capabilities: CapabilitySet::default(),
740 ..with_ob
741 };
742 assert!(!without.has_order_book());
743 }
744
745 #[test]
746 fn test_has_ticker() {
747 let with_ticker = VenueDescriptor {
748 id: "x".to_string(),
749 name: "X".to_string(),
750 base_url: "https://x.com".to_string(),
751 timeout_secs: None,
752 rate_limit_per_sec: None,
753 symbol: SymbolConfig {
754 template: "{base}{quote}".to_string(),
755 default_quote: "USDT".to_string(),
756 case: SymbolCase::Upper,
757 },
758 headers: HashMap::new(),
759 capabilities: CapabilitySet {
760 order_book: None,
761 ticker: Some(EndpointDescriptor {
762 method: HttpMethod::GET,
763 path: "/ticker".to_string(),
764 params: HashMap::new(),
765 request_body: None,
766 response_root: None,
767 response: ResponseMapping::default(),
768 }),
769 trades: None,
770 ohlc: None,
771 },
772 };
773 assert!(with_ticker.has_ticker());
774
775 let without = VenueDescriptor {
776 capabilities: CapabilitySet::default(),
777 ..with_ticker
778 };
779 assert!(!without.has_ticker());
780 }
781
782 #[test]
783 fn test_has_trades() {
784 let with_trades = VenueDescriptor {
785 id: "x".to_string(),
786 name: "X".to_string(),
787 base_url: "https://x.com".to_string(),
788 timeout_secs: None,
789 rate_limit_per_sec: None,
790 symbol: SymbolConfig {
791 template: "{base}{quote}".to_string(),
792 default_quote: "USDT".to_string(),
793 case: SymbolCase::Upper,
794 },
795 headers: HashMap::new(),
796 capabilities: CapabilitySet {
797 order_book: None,
798 ticker: None,
799 trades: Some(EndpointDescriptor {
800 method: HttpMethod::GET,
801 path: "/trades".to_string(),
802 params: HashMap::new(),
803 request_body: None,
804 response_root: None,
805 response: ResponseMapping::default(),
806 }),
807 ohlc: None,
808 },
809 };
810 assert!(with_trades.has_trades());
811
812 let without = VenueDescriptor {
813 capabilities: CapabilitySet::default(),
814 ..with_trades
815 };
816 assert!(!without.has_trades());
817 }
818
819 #[test]
820 fn test_has_ohlc() {
821 let with_ohlc = VenueDescriptor {
822 id: "x".to_string(),
823 name: "X".to_string(),
824 base_url: "https://x.com".to_string(),
825 timeout_secs: None,
826 rate_limit_per_sec: None,
827 symbol: SymbolConfig {
828 template: "{base}{quote}".to_string(),
829 default_quote: "USDT".to_string(),
830 case: SymbolCase::Upper,
831 },
832 headers: HashMap::new(),
833 capabilities: CapabilitySet {
834 order_book: None,
835 ticker: None,
836 trades: None,
837 ohlc: Some(EndpointDescriptor {
838 method: HttpMethod::GET,
839 path: "/klines".to_string(),
840 params: HashMap::new(),
841 request_body: None,
842 response_root: None,
843 response: ResponseMapping::default(),
844 }),
845 },
846 };
847 assert!(with_ohlc.has_ohlc());
848 assert!(with_ohlc.capability_names().contains(&"ohlc"));
849
850 let without = VenueDescriptor {
851 capabilities: CapabilitySet::default(),
852 ..with_ohlc
853 };
854 assert!(!without.has_ohlc());
855 assert!(!without.capability_names().contains(&"ohlc"));
856 }
857
858 #[test]
859 fn test_response_mapping_default() {
860 let m = ResponseMapping::default();
861 assert!(m.asks_key.is_none());
862 assert!(m.bids_key.is_none());
863 assert!(m.level_format.is_none());
864 assert!(m.side.is_none());
865 }
866
867 #[test]
868 fn test_capability_set_default() {
869 let c = CapabilitySet::default();
870 assert!(c.order_book.is_none());
871 assert!(c.ticker.is_none());
872 assert!(c.trades.is_none());
873 assert!(c.ohlc.is_none());
874 }
875
876 #[test]
877 fn test_deserialize_ohlc_capability() {
878 let yaml = r#"
879id: ohlc_venue
880name: OHLC Venue Test
881base_url: https://api.example.com
882
883symbol:
884 template: "{base}{quote}"
885 default_quote: USDT
886
887capabilities:
888 order_book:
889 path: /depth
890 params:
891 symbol: "{pair}"
892 response:
893 asks_key: asks
894 bids_key: bids
895 level_format: positional
896 ohlc:
897 path: /api/v3/klines
898 params:
899 symbol: "{pair}"
900 interval: "{interval}"
901 limit: "{limit}"
902 response:
903 ohlc_format: array_of_arrays
904 ohlc_fields: [open_time, open, high, low, close, volume, close_time]
905"#;
906 let desc: VenueDescriptor = serde_yaml::from_str(yaml).unwrap();
907 assert!(desc.has_ohlc());
908 assert!(desc.capability_names().contains(&"ohlc"));
909 let ohlc = desc.capabilities.ohlc.as_ref().unwrap();
910 assert_eq!(ohlc.path, "/api/v3/klines");
911 assert_eq!(ohlc.params.get("interval"), Some(&"{interval}".to_string()));
912 }
913
914 #[test]
915 fn test_deserialize_full_descriptor_yaml() {
916 let yaml = r#"
917id: test_venue
918name: Test Venue Full
919base_url: https://api.test.com
920timeout_secs: 30
921rate_limit_per_sec: 5
922
923symbol:
924 template: "{base}-{quote}"
925 default_quote: USD
926 case: upper
927
928headers:
929 X-API-KEY: "placeholder"
930
931capabilities:
932 order_book:
933 path: /book
934 params:
935 pair: "{pair}"
936 response:
937 asks_key: asks
938 bids_key: bids
939 ticker:
940 path: /ticker
941 response:
942 last_price: last
943 trades:
944 path: /trades
945 response:
946 price: p
947 quantity: q
948"#;
949 let desc: VenueDescriptor = serde_yaml::from_str(yaml).unwrap();
950 assert_eq!(desc.id, "test_venue");
951 assert_eq!(desc.name, "Test Venue Full");
952 assert_eq!(desc.base_url, "https://api.test.com");
953 assert_eq!(desc.timeout_secs, Some(30));
954 assert_eq!(desc.rate_limit_per_sec, Some(5));
955 assert_eq!(desc.symbol.template, "{base}-{quote}");
956 assert_eq!(desc.symbol.default_quote, "USD");
957 assert_eq!(
958 desc.headers.get("X-API-KEY"),
959 Some(&"placeholder".to_string())
960 );
961 assert!(desc.has_order_book());
962 assert!(desc.has_ticker());
963 assert!(desc.has_trades());
964 assert_eq!(
965 desc.capability_names(),
966 vec!["order_book", "ticker", "trades"]
967 );
968 }
969}