1use std::fmt;
10
11use polyoxide_venue::positional::{drain, element, DecimalStr};
12use rust_decimal::Decimal;
13use serde::{
14 de::{self, DeserializeOwned, SeqAccess, Visitor},
15 ser::SerializeSeq,
16 Deserialize, Deserializer, Serialize, Serializer,
17};
18use serde_json::Value;
19
20pub use crate::shared::symbol::{InvalidSymbol, Symbol};
21
22polyoxide_venue::wire_enum! {
23 pub enum Interval {
26 M1 => "1m", M3 => "3m", M5 => "5m", M15 => "15m", M30 => "30m",
27 H1 => "1h", H2 => "2h", H4 => "4h", H6 => "6h", H8 => "8h", H12 => "12h",
28 D1 => "1d", D3 => "3d", W1 => "1w",
29 Mo1 => "1M",
31 }
32}
33
34polyoxide_venue::wire_enum! {
35 pub enum DepthLimit {
38 Five => "5", Ten => "10", Twenty => "20", Fifty => "50",
39 Hundred => "100", FiveHundred => "500", Thousand => "1000",
40 }
41}
42
43polyoxide_venue::open_enum! {
44 pub enum ContractType {
46 Perpetual => "PERPETUAL",
47 TradifiPerpetual => "TRADIFI_PERPETUAL",
49 CurrentMonth => "CURRENT_MONTH",
50 NextMonth => "NEXT_MONTH",
51 CurrentQuarter => "CURRENT_QUARTER",
52 NextQuarter => "NEXT_QUARTER",
53 PerpetualDelivering => "PERPETUAL_DELIVERING",
54 }
55}
56
57polyoxide_venue::open_enum! {
58 pub enum SymbolStatus {
62 PendingTrading => "PENDING_TRADING",
63 Trading => "TRADING",
64 PreDelivering => "PRE_DELIVERING",
65 Delivering => "DELIVERING",
66 Delivered => "DELIVERED",
67 PreSettle => "PRE_SETTLE",
68 Settling => "SETTLING",
70 Close => "CLOSE",
71 TradingHalt => "TRADING_HALT",
72 TradingCancelOnly => "TRADING_CANCEL_ONLY",
73 }
74}
75
76polyoxide_venue::open_enum! {
77 pub enum UnderlyingType {
80 Coin => "COIN",
81 Index => "INDEX",
82 Premarket => "PREMARKET",
83 Commodity => "COMMODITY",
84 Equity => "EQUITY",
85 CnEquity => "CN_EQUITY",
86 HkEquity => "HK_EQUITY",
87 KrEquity => "KR_EQUITY",
88 Fx => "FX",
89 }
90}
91
92mod decimal_or_empty {
97 use super::*;
98
99 pub fn serialize<S: Serializer>(
100 value: &Option<Decimal>,
101 serializer: S,
102 ) -> Result<S::Ok, S::Error> {
103 match value {
104 Some(value) => serializer.collect_str(value),
105 None => serializer.serialize_str(""),
106 }
107 }
108
109 pub fn deserialize<'de, D: Deserializer<'de>>(
110 deserializer: D,
111 ) -> Result<Option<Decimal>, D::Error> {
112 let raw = String::deserialize(deserializer)?;
113 if raw.is_empty() {
114 return Ok(None);
115 }
116 raw.parse().map(Some).map_err(de::Error::custom)
117 }
118}
119
120#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
122#[serde(rename_all = "camelCase")]
123#[non_exhaustive]
124pub struct ServerTime {
125 pub server_time: u64,
127}
128
129#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
131#[serde(rename_all = "camelCase")]
132#[non_exhaustive]
133pub struct ExchangeInfo {
134 pub timezone: String,
136 pub server_time: u64,
138 pub futures_type: String,
140 pub rate_limits: Vec<RateLimit>,
142 pub exchange_filters: Vec<Filter>,
144 pub assets: Vec<AssetInfo>,
146 pub symbols: Vec<SymbolInfo>,
148}
149
150#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
152#[serde(rename_all = "camelCase")]
153#[non_exhaustive]
154pub struct RateLimit {
155 pub rate_limit_type: String,
157 pub interval: String,
159 pub interval_num: u32,
161 pub limit: u32,
163}
164
165#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
167#[serde(rename_all = "camelCase")]
168#[non_exhaustive]
169pub struct AssetInfo {
170 pub asset: String,
172 pub margin_available: bool,
174 #[serde(with = "rust_decimal::serde::str")]
176 pub auto_asset_exchange: Decimal,
177}
178
179#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
181#[serde(rename_all = "camelCase")]
182#[non_exhaustive]
183pub struct SymbolInfo {
184 pub symbol: String,
186 pub pair: String,
188 pub contract_type: ContractType,
190 pub delivery_date: u64,
192 pub onboard_date: u64,
194 pub status: SymbolStatus,
196 #[serde(with = "rust_decimal::serde::str")]
198 pub maint_margin_percent: Decimal,
199 #[serde(with = "rust_decimal::serde::str")]
201 pub required_margin_percent: Decimal,
202 pub base_asset: String,
204 pub quote_asset: String,
206 pub margin_asset: String,
208 pub price_precision: u32,
210 pub quantity_precision: u32,
212 pub base_asset_precision: u32,
214 pub quote_precision: u32,
216 pub underlying_type: UnderlyingType,
218 pub underlying_sub_type: Vec<String>,
220 #[serde(with = "rust_decimal::serde::str")]
222 pub trigger_protect: Decimal,
223 #[serde(with = "rust_decimal::serde::str")]
225 pub liquidation_fee: Decimal,
226 #[serde(with = "rust_decimal::serde::str")]
228 pub market_take_bound: Decimal,
229 pub max_move_order_limit: u32,
231 pub filters: Vec<Filter>,
233 pub order_types: Vec<String>,
235 pub time_in_force: Vec<String>,
237 pub permission_sets: Vec<String>,
239}
240
241#[derive(Debug, Clone, PartialEq)]
248#[non_exhaustive]
249pub enum Filter {
250 #[non_exhaustive]
252 PriceFilter {
253 min_price: Decimal,
255 max_price: Decimal,
257 tick_size: Decimal,
259 },
260 #[non_exhaustive]
262 LotSize {
263 min_qty: Decimal,
265 max_qty: Decimal,
267 step_size: Decimal,
269 },
270 #[non_exhaustive]
272 MarketLotSize {
273 min_qty: Decimal,
275 max_qty: Decimal,
277 step_size: Decimal,
279 },
280 #[non_exhaustive]
282 MaxNumOrders {
283 limit: u32,
285 },
286 #[non_exhaustive]
288 MinNotional {
289 notional: Decimal,
291 },
292 #[non_exhaustive]
294 PercentPrice {
295 multiplier_up: Decimal,
297 multiplier_down: Decimal,
299 multiplier_decimal: Decimal,
301 },
302 #[non_exhaustive]
304 PositionRiskControl {
305 position_control_side: String,
307 },
308 Other {
310 filter_type: String,
312 raw: Value,
314 },
315}
316
317mod filter_wire {
319 use super::*;
320
321 #[derive(Serialize, Deserialize)]
322 #[serde(rename_all = "camelCase")]
323 pub struct Price {
324 #[serde(with = "rust_decimal::serde::str")]
325 pub min_price: Decimal,
326 #[serde(with = "rust_decimal::serde::str")]
327 pub max_price: Decimal,
328 #[serde(with = "rust_decimal::serde::str")]
329 pub tick_size: Decimal,
330 }
331
332 #[derive(Serialize, Deserialize)]
333 #[serde(rename_all = "camelCase")]
334 pub struct Lot {
335 #[serde(with = "rust_decimal::serde::str")]
336 pub min_qty: Decimal,
337 #[serde(with = "rust_decimal::serde::str")]
338 pub max_qty: Decimal,
339 #[serde(with = "rust_decimal::serde::str")]
340 pub step_size: Decimal,
341 }
342
343 #[derive(Serialize, Deserialize)]
344 pub struct MaxNumOrders {
345 pub limit: u32,
346 }
347
348 #[derive(Serialize, Deserialize)]
349 pub struct MinNotional {
350 #[serde(with = "rust_decimal::serde::str")]
351 pub notional: Decimal,
352 }
353
354 #[derive(Serialize, Deserialize)]
355 #[serde(rename_all = "camelCase")]
356 pub struct PercentPrice {
357 #[serde(with = "rust_decimal::serde::str")]
358 pub multiplier_up: Decimal,
359 #[serde(with = "rust_decimal::serde::str")]
360 pub multiplier_down: Decimal,
361 #[serde(with = "rust_decimal::serde::str")]
362 pub multiplier_decimal: Decimal,
363 }
364
365 #[derive(Serialize, Deserialize)]
366 #[serde(rename_all = "camelCase")]
367 pub struct PositionRiskControl {
368 pub position_control_side: String,
369 }
370}
371
372impl<'de> Deserialize<'de> for Filter {
373 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
374 use filter_wire as wire;
375
376 fn parse<T: DeserializeOwned, E: de::Error>(raw: Value) -> Result<T, E> {
377 serde_json::from_value(raw).map_err(E::custom)
378 }
379
380 let raw = Value::deserialize(deserializer)?;
381 let filter_type = raw
382 .get("filterType")
383 .and_then(Value::as_str)
384 .ok_or_else(|| de::Error::custom("a filter without a string filterType"))?
385 .to_owned();
386 Ok(match filter_type.as_str() {
387 "PRICE_FILTER" => {
388 let f: wire::Price = parse(raw)?;
389 Self::PriceFilter {
390 min_price: f.min_price,
391 max_price: f.max_price,
392 tick_size: f.tick_size,
393 }
394 }
395 "LOT_SIZE" => {
396 let f: wire::Lot = parse(raw)?;
397 Self::LotSize {
398 min_qty: f.min_qty,
399 max_qty: f.max_qty,
400 step_size: f.step_size,
401 }
402 }
403 "MARKET_LOT_SIZE" => {
404 let f: wire::Lot = parse(raw)?;
405 Self::MarketLotSize {
406 min_qty: f.min_qty,
407 max_qty: f.max_qty,
408 step_size: f.step_size,
409 }
410 }
411 "MAX_NUM_ORDERS" => {
412 let f: wire::MaxNumOrders = parse(raw)?;
413 Self::MaxNumOrders { limit: f.limit }
414 }
415 "MIN_NOTIONAL" => {
416 let f: wire::MinNotional = parse(raw)?;
417 Self::MinNotional {
418 notional: f.notional,
419 }
420 }
421 "PERCENT_PRICE" => {
422 let f: wire::PercentPrice = parse(raw)?;
423 Self::PercentPrice {
424 multiplier_up: f.multiplier_up,
425 multiplier_down: f.multiplier_down,
426 multiplier_decimal: f.multiplier_decimal,
427 }
428 }
429 "POSITION_RISK_CONTROL" => {
430 let f: wire::PositionRiskControl = parse(raw)?;
431 Self::PositionRiskControl {
432 position_control_side: f.position_control_side,
433 }
434 }
435 _ => Self::Other { filter_type, raw },
436 })
437 }
438}
439
440impl Serialize for Filter {
441 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
442 use filter_wire as wire;
443
444 fn tagged<T: Serialize, E: serde::ser::Error>(
445 filter_type: &str,
446 body: &T,
447 ) -> Result<Value, E> {
448 let mut value = serde_json::to_value(body).map_err(E::custom)?;
449 if let Value::Object(map) = &mut value {
450 map.insert(
451 "filterType".to_owned(),
452 Value::String(filter_type.to_owned()),
453 );
454 }
455 Ok(value)
456 }
457
458 let value = match self {
459 Self::PriceFilter {
460 min_price,
461 max_price,
462 tick_size,
463 } => tagged(
464 "PRICE_FILTER",
465 &wire::Price {
466 min_price: *min_price,
467 max_price: *max_price,
468 tick_size: *tick_size,
469 },
470 )?,
471 Self::LotSize {
472 min_qty,
473 max_qty,
474 step_size,
475 } => tagged(
476 "LOT_SIZE",
477 &wire::Lot {
478 min_qty: *min_qty,
479 max_qty: *max_qty,
480 step_size: *step_size,
481 },
482 )?,
483 Self::MarketLotSize {
484 min_qty,
485 max_qty,
486 step_size,
487 } => tagged(
488 "MARKET_LOT_SIZE",
489 &wire::Lot {
490 min_qty: *min_qty,
491 max_qty: *max_qty,
492 step_size: *step_size,
493 },
494 )?,
495 Self::MaxNumOrders { limit } => {
496 tagged("MAX_NUM_ORDERS", &wire::MaxNumOrders { limit: *limit })?
497 }
498 Self::MinNotional { notional } => tagged(
499 "MIN_NOTIONAL",
500 &wire::MinNotional {
501 notional: *notional,
502 },
503 )?,
504 Self::PercentPrice {
505 multiplier_up,
506 multiplier_down,
507 multiplier_decimal,
508 } => tagged(
509 "PERCENT_PRICE",
510 &wire::PercentPrice {
511 multiplier_up: *multiplier_up,
512 multiplier_down: *multiplier_down,
513 multiplier_decimal: *multiplier_decimal,
514 },
515 )?,
516 Self::PositionRiskControl {
517 position_control_side,
518 } => tagged(
519 "POSITION_RISK_CONTROL",
520 &wire::PositionRiskControl {
521 position_control_side: position_control_side.clone(),
522 },
523 )?,
524 Self::Other { raw, .. } => raw.clone(),
525 };
526 value.serialize(serializer)
527 }
528}
529
530#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
533#[serde(rename_all = "camelCase")]
534#[non_exhaustive]
535pub struct FundingInfo {
536 pub symbol: String,
539 #[serde(with = "rust_decimal::serde::str")]
541 pub adjusted_funding_rate_cap: Decimal,
542 #[serde(with = "rust_decimal::serde::str")]
544 pub adjusted_funding_rate_floor: Decimal,
545 pub funding_interval_hours: u32,
547 pub disclaimer: bool,
549 pub update_time: Option<u64>,
552}
553
554#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
556#[serde(rename_all = "camelCase")]
557#[non_exhaustive]
558pub struct Ticker24h {
559 pub symbol: String,
561 #[serde(with = "rust_decimal::serde::str")]
563 pub price_change: Decimal,
564 #[serde(with = "rust_decimal::serde::str")]
566 pub price_change_percent: Decimal,
567 #[serde(with = "rust_decimal::serde::str")]
569 pub weighted_avg_price: Decimal,
570 #[serde(with = "rust_decimal::serde::str")]
572 pub last_price: Decimal,
573 #[serde(with = "rust_decimal::serde::str")]
575 pub last_qty: Decimal,
576 #[serde(with = "rust_decimal::serde::str")]
578 pub open_price: Decimal,
579 #[serde(with = "rust_decimal::serde::str")]
581 pub high_price: Decimal,
582 #[serde(with = "rust_decimal::serde::str")]
584 pub low_price: Decimal,
585 #[serde(with = "rust_decimal::serde::str")]
587 pub volume: Decimal,
588 #[serde(with = "rust_decimal::serde::str")]
590 pub quote_volume: Decimal,
591 pub open_time: u64,
593 pub close_time: u64,
595 pub first_id: i64,
597 pub last_id: i64,
599 pub count: u64,
601}
602
603#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
605#[serde(rename_all = "camelCase")]
606#[non_exhaustive]
607pub struct PremiumIndex {
608 pub symbol: String,
610 #[serde(with = "rust_decimal::serde::str")]
612 pub mark_price: Decimal,
613 #[serde(with = "rust_decimal::serde::str")]
615 pub index_price: Decimal,
616 #[serde(with = "rust_decimal::serde::str")]
618 pub estimated_settle_price: Decimal,
619 #[serde(with = "rust_decimal::serde::str")]
621 pub last_funding_rate: Decimal,
622 #[serde(with = "rust_decimal::serde::str")]
624 pub interest_rate: Decimal,
625 pub next_funding_time: u64,
627 pub time: u64,
629}
630
631#[derive(Debug, Clone, PartialEq, Eq)]
636#[non_exhaustive]
637pub struct Kline {
638 pub open_time: u64,
640 pub open: Decimal,
642 pub high: Decimal,
644 pub low: Decimal,
646 pub close: Decimal,
648 pub volume: Decimal,
650 pub close_time: u64,
652 pub quote_volume: Decimal,
654 pub trade_count: u64,
656 pub taker_buy_base_volume: Decimal,
658 pub taker_buy_quote_volume: Decimal,
660}
661
662impl<'de> Deserialize<'de> for Kline {
663 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
664 struct KlineVisitor;
665
666 impl<'de> Visitor<'de> for KlineVisitor {
667 type Value = Kline;
668
669 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
670 f.write_str("a kline array of at least 11 elements")
671 }
672
673 fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Kline, A::Error> {
674 const WHAT: &str = "a kline array of at least 11 elements";
675 let kline = Kline {
676 open_time: element(&mut seq, 0, WHAT)?,
677 open: element::<DecimalStr, _>(&mut seq, 1, WHAT)?.0,
678 high: element::<DecimalStr, _>(&mut seq, 2, WHAT)?.0,
679 low: element::<DecimalStr, _>(&mut seq, 3, WHAT)?.0,
680 close: element::<DecimalStr, _>(&mut seq, 4, WHAT)?.0,
681 volume: element::<DecimalStr, _>(&mut seq, 5, WHAT)?.0,
682 close_time: element(&mut seq, 6, WHAT)?,
683 quote_volume: element::<DecimalStr, _>(&mut seq, 7, WHAT)?.0,
684 trade_count: element(&mut seq, 8, WHAT)?,
685 taker_buy_base_volume: element::<DecimalStr, _>(&mut seq, 9, WHAT)?.0,
686 taker_buy_quote_volume: element::<DecimalStr, _>(&mut seq, 10, WHAT)?.0,
687 };
688 drain(&mut seq)?;
689 Ok(kline)
690 }
691 }
692
693 deserializer.deserialize_seq(KlineVisitor)
694 }
695}
696
697impl Serialize for Kline {
698 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
699 let mut seq = serializer.serialize_seq(Some(11))?;
700 seq.serialize_element(&self.open_time)?;
701 for value in [&self.open, &self.high, &self.low, &self.close, &self.volume] {
702 seq.serialize_element(&value.to_string())?;
703 }
704 seq.serialize_element(&self.close_time)?;
705 seq.serialize_element(&self.quote_volume.to_string())?;
706 seq.serialize_element(&self.trade_count)?;
707 seq.serialize_element(&self.taker_buy_base_volume.to_string())?;
708 seq.serialize_element(&self.taker_buy_quote_volume.to_string())?;
709 seq.end()
710 }
711}
712
713#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
715#[serde(rename_all = "camelCase")]
716#[non_exhaustive]
717pub struct FundingRate {
718 pub symbol: String,
720 pub funding_time: u64,
722 #[serde(with = "rust_decimal::serde::str")]
724 pub funding_rate: Decimal,
725 #[serde(with = "decimal_or_empty")]
728 pub mark_price: Option<Decimal>,
729 pub rate_type: String,
731}
732
733#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
735#[serde(rename_all = "camelCase")]
736#[non_exhaustive]
737pub struct OpenInterest {
738 pub symbol: String,
740 #[serde(with = "rust_decimal::serde::str")]
742 pub open_interest: Decimal,
743 pub time: u64,
745}
746
747#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
750#[non_exhaustive]
751pub struct AggTrade {
752 #[serde(rename = "a")]
754 pub id: u64,
755 #[serde(rename = "p", with = "rust_decimal::serde::str")]
757 pub price: Decimal,
758 #[serde(rename = "q", with = "rust_decimal::serde::str")]
760 pub quantity: Decimal,
761 #[serde(rename = "nq", with = "rust_decimal::serde::str")]
764 pub normal_quantity: Decimal,
765 #[serde(rename = "f")]
767 pub first_trade_id: u64,
768 #[serde(rename = "l")]
770 pub last_trade_id: u64,
771 #[serde(rename = "T")]
773 pub time: u64,
774 #[serde(rename = "m")]
776 pub is_buyer_maker: bool,
777}
778
779#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
781#[non_exhaustive]
782pub struct Depth {
783 #[serde(rename = "lastUpdateId")]
785 pub last_update_id: u64,
786 #[serde(rename = "E")]
788 pub event_time: u64,
789 #[serde(rename = "T")]
791 pub transaction_time: u64,
792 pub bids: Vec<Level>,
794 pub asks: Vec<Level>,
796}
797
798#[derive(Debug, Clone, Copy, PartialEq, Eq)]
800#[non_exhaustive]
801pub struct Level {
802 pub price: Decimal,
804 pub quantity: Decimal,
806}
807
808impl Level {
809 pub fn new(price: Decimal, quantity: Decimal) -> Self {
811 Self { price, quantity }
812 }
813}
814
815impl<'de> Deserialize<'de> for Level {
816 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
817 struct LevelVisitor;
818
819 impl<'de> Visitor<'de> for LevelVisitor {
820 type Value = Level;
821
822 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
823 f.write_str("a [price, quantity] array")
824 }
825
826 fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Level, A::Error> {
827 const WHAT: &str = "a [price, quantity] array";
828 let level = Level {
829 price: element::<DecimalStr, _>(&mut seq, 0, WHAT)?.0,
830 quantity: element::<DecimalStr, _>(&mut seq, 1, WHAT)?.0,
831 };
832 drain(&mut seq)?;
833 Ok(level)
834 }
835 }
836
837 deserializer.deserialize_seq(LevelVisitor)
838 }
839}
840
841impl Serialize for Level {
842 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
843 let mut seq = serializer.serialize_seq(Some(2))?;
844 seq.serialize_element(&self.price.to_string())?;
845 seq.serialize_element(&self.quantity.to_string())?;
846 seq.end()
847 }
848}
849
850#[cfg(test)]
851mod tests {
852 use super::*;
853
854 #[test]
855 fn a_refused_symbol_and_an_unknown_spelling_are_invalid_requests() {
856 use polyoxide_venue::{Class, Classify, UnknownVariant};
857
858 let symbol = Symbol::new("BTC USDT").unwrap_err();
859 assert_eq!(symbol.class(), Class::InvalidRequest);
860 assert!(symbol.is_fault());
861 assert_eq!(symbol.retry_after(), None);
862 assert!(!symbol.is_retriable());
863
864 let spelling = UnknownVariant {
865 type_name: "Interval",
866 value: "2m".into(),
867 };
868 assert_eq!(spelling.class(), Class::InvalidRequest);
869 assert!(spelling.is_fault());
870 assert_eq!(spelling.retry_after(), None);
871 assert!(!spelling.is_retriable());
872 }
873
874 #[test]
875 fn symbols_take_letters_digits_and_underscores_in_any_script() {
876 for good in ["BTCUSDT", "BTCUSDT_261225", "币安人生USDT", "1000PEPEUSDT"] {
877 assert_eq!(Symbol::new(good).unwrap().as_str(), good);
878 }
879 for bad in [
880 "",
881 "BTC USDT",
882 "BTC@USDT",
883 "BTC/USDT",
884 "BTC-USDT",
885 &"A".repeat(33),
886 ] {
887 assert_eq!(
888 Symbol::new(bad),
889 Err(InvalidSymbol(bad.to_owned())),
890 "{bad:?}"
891 );
892 }
893 assert!(Symbol::new("A".repeat(32)).is_ok());
894 }
895
896 #[test]
897 fn ascii_letters_are_uppercased_and_nothing_else_changes() {
898 assert_eq!(
899 Symbol::new("btcusdt").unwrap(),
900 Symbol::new("BTCUSDT").unwrap()
901 );
902 assert_eq!(
903 Symbol::new("btcusdt_261225").unwrap().as_str(),
904 "BTCUSDT_261225"
905 );
906 assert_eq!(
907 Symbol::new("币安人生usdt").unwrap().as_str(),
908 "币安人生USDT"
909 );
910 }
911
912 #[test]
913 fn symbols_are_measured_in_characters_and_only_ascii_changes_case() {
914 assert_eq!(Symbol::new("straße").unwrap().as_str(), "STRAßE");
916 assert!(Symbol::new("币".repeat(32)).is_ok());
917 assert!(Symbol::new("币".repeat(33)).is_err());
918 }
919
920 #[test]
921 fn wire_spellings_are_binance_s() {
922 let intervals: Vec<&str> = Interval::ALL.iter().map(|i| i.as_str()).collect();
925 assert_eq!(
926 intervals,
927 [
928 "1m", "3m", "5m", "15m", "30m", "1h", "2h", "4h", "6h", "8h", "12h", "1d", "3d",
929 "1w", "1M"
930 ]
931 );
932 let limits: Vec<&str> = DepthLimit::ALL.iter().map(|l| l.as_str()).collect();
933 assert_eq!(limits, ["5", "10", "20", "50", "100", "500", "1000"]);
934 }
935
936 #[test]
937 fn every_interval_round_trips_its_wire_spelling() {
938 assert_eq!(Interval::ALL.len(), 15);
939 for interval in Interval::ALL {
940 assert_eq!(interval.as_str().parse::<Interval>(), Ok(*interval));
941 }
942 assert!("1s".parse::<Interval>().is_err(), "this host refuses 1s");
943 }
944
945 #[test]
946 fn an_unknown_contract_type_is_kept_verbatim() {
947 let parsed: ContractType = serde_json::from_str(r#""NEXT_DECADE""#).unwrap();
948 assert_eq!(parsed, ContractType::Other("NEXT_DECADE".to_owned()));
949 assert_eq!(serde_json::to_string(&parsed).unwrap(), r#""NEXT_DECADE""#);
950 let known: ContractType = serde_json::from_str(r#""TRADIFI_PERPETUAL""#).unwrap();
951 assert_eq!(known, ContractType::TradifiPerpetual);
952 }
953
954 #[test]
955 fn an_unseen_filter_type_is_kept_and_the_known_ones_parse() {
956 let filters: Vec<Filter> = serde_json::from_str(
957 r#"[{"filterType":"PRICE_FILTER","minPrice":"261.10","maxPrice":"809484","tickSize":"0.10"},
958 {"filterType":"MAX_NUM_ICEBERG_ORDERS","maxNumIcebergOrders":5}]"#,
959 )
960 .unwrap();
961 assert!(
962 matches!(&filters[0], Filter::PriceFilter { tick_size, .. } if *tick_size == Decimal::new(10, 2))
963 );
964 let Filter::Other { filter_type, raw } = &filters[1] else {
965 panic!("an unseen filter type must not fail the response");
966 };
967 assert_eq!(filter_type, "MAX_NUM_ICEBERG_ORDERS");
968 assert_eq!(raw["maxNumIcebergOrders"], 5);
969 let again: Vec<Value> =
970 serde_json::from_value(serde_json::to_value(&filters).unwrap()).unwrap();
971 assert_eq!(again[0]["filterType"], "PRICE_FILTER");
972 assert_eq!(again[1]["maxNumIcebergOrders"], 5);
973 }
974
975 #[test]
976 fn a_known_filter_with_a_missing_field_fails_loudly() {
977 let err = serde_json::from_str::<Filter>(r#"{"filterType":"LOT_SIZE","minQty":"0.001"}"#)
978 .unwrap_err();
979 assert!(err.to_string().contains("maxQty"), "{err}");
980 }
981
982 #[test]
983 fn a_kline_drops_the_ignored_element_and_tolerates_more() {
984 let wire = r#"[1791354000000,"84125.70","84125.70","84099.90","84106.90","214.561",1791354059999,"18046628.85200",2912,"38.078","3202701.86390","0","extra"]"#;
985 let kline: Kline = serde_json::from_str(wire).unwrap();
986 assert_eq!(kline.close, "84106.90".parse::<Decimal>().unwrap());
987 assert_eq!(kline.trade_count, 2912);
988 assert_eq!(
989 serde_json::to_string(&kline).unwrap(),
990 r#"[1791354000000,"84125.70","84125.70","84099.90","84106.90","214.561",1791354059999,"18046628.85200",2912,"38.078","3202701.86390"]"#
991 );
992 assert!(serde_json::from_str::<Kline>("[1791354000000]").is_err());
993 }
994
995 #[test]
996 fn a_level_keeps_every_digit() {
997 let level: Level = serde_json::from_str(r#"["84141.123456789012345","14.663"]"#).unwrap();
999 assert_eq!(level.price.to_string(), "84141.123456789012345");
1000 assert_eq!(
1001 serde_json::to_string(&level).unwrap(),
1002 r#"["84141.123456789012345","14.663"]"#
1003 );
1004 }
1005
1006 #[test]
1007 fn an_old_funding_rate_has_no_mark_price() {
1008 let old: FundingRate = serde_json::from_str(
1010 r#"{"symbol":"BTCUSDT","fundingTime":1568102400000,"fundingRate":"0.00010000","markPrice":"","rateType":"Regular"}"#,
1011 )
1012 .unwrap();
1013 assert_eq!(old.mark_price, None);
1014 assert_eq!(serde_json::to_value(&old).unwrap()["markPrice"], "");
1015
1016 let new: FundingRate = serde_json::from_str(
1017 r#"{"symbol":"BTCUSDT","fundingTime":1791273600001,"fundingRate":"-0.00001592","markPrice":"85514.00007496","rateType":"Regular"}"#,
1018 )
1019 .unwrap();
1020 assert_eq!(new.mark_price, Some("85514.00007496".parse().unwrap()));
1021 }
1022}