1use std::{fmt, str::FromStr};
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
20#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
33pub struct Symbol(String);
34
35#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
37#[error("{0:?} is not a Binance symbol: 1 to 32 letters, digits or underscores")]
38pub struct InvalidSymbol(pub String);
39
40impl polyoxide_venue::Classify for InvalidSymbol {
42 fn class(&self) -> polyoxide_venue::Class {
43 polyoxide_venue::Class::InvalidRequest
44 }
45}
46
47impl Symbol {
48 pub const MAX_LEN: usize = 32;
50
51 pub fn new(symbol: impl Into<String>) -> Result<Self, InvalidSymbol> {
53 let symbol = symbol.into();
54 let chars = symbol.chars().count();
55 let valid = (1..=Self::MAX_LEN).contains(&chars)
56 && symbol.chars().all(|c| c.is_alphanumeric() || c == '_');
57 if valid {
58 Ok(Self(symbol.to_ascii_uppercase()))
59 } else {
60 Err(InvalidSymbol(symbol))
61 }
62 }
63
64 pub fn as_str(&self) -> &str {
66 &self.0
67 }
68}
69
70impl fmt::Display for Symbol {
71 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72 f.write_str(&self.0)
73 }
74}
75
76impl FromStr for Symbol {
77 type Err = InvalidSymbol;
78
79 fn from_str(s: &str) -> Result<Self, Self::Err> {
80 Self::new(s)
81 }
82}
83
84impl AsRef<str> for Symbol {
85 fn as_ref(&self) -> &str {
86 &self.0
87 }
88}
89
90polyoxide_venue::wire_enum! {
91 pub enum Interval {
94 M1 => "1m", M3 => "3m", M5 => "5m", M15 => "15m", M30 => "30m",
95 H1 => "1h", H2 => "2h", H4 => "4h", H6 => "6h", H8 => "8h", H12 => "12h",
96 D1 => "1d", D3 => "3d", W1 => "1w",
97 Mo1 => "1M",
99 }
100}
101
102polyoxide_venue::wire_enum! {
103 pub enum DepthLimit {
106 Five => "5", Ten => "10", Twenty => "20", Fifty => "50",
107 Hundred => "100", FiveHundred => "500", Thousand => "1000",
108 }
109}
110
111polyoxide_venue::open_enum! {
112 pub enum ContractType {
114 Perpetual => "PERPETUAL",
115 TradifiPerpetual => "TRADIFI_PERPETUAL",
117 CurrentMonth => "CURRENT_MONTH",
118 NextMonth => "NEXT_MONTH",
119 CurrentQuarter => "CURRENT_QUARTER",
120 NextQuarter => "NEXT_QUARTER",
121 PerpetualDelivering => "PERPETUAL_DELIVERING",
122 }
123}
124
125polyoxide_venue::open_enum! {
126 pub enum SymbolStatus {
130 PendingTrading => "PENDING_TRADING",
131 Trading => "TRADING",
132 PreDelivering => "PRE_DELIVERING",
133 Delivering => "DELIVERING",
134 Delivered => "DELIVERED",
135 PreSettle => "PRE_SETTLE",
136 Settling => "SETTLING",
138 Close => "CLOSE",
139 TradingHalt => "TRADING_HALT",
140 TradingCancelOnly => "TRADING_CANCEL_ONLY",
141 }
142}
143
144polyoxide_venue::open_enum! {
145 pub enum UnderlyingType {
148 Coin => "COIN",
149 Index => "INDEX",
150 Premarket => "PREMARKET",
151 Commodity => "COMMODITY",
152 Equity => "EQUITY",
153 CnEquity => "CN_EQUITY",
154 HkEquity => "HK_EQUITY",
155 KrEquity => "KR_EQUITY",
156 Fx => "FX",
157 }
158}
159
160mod decimal_or_empty {
165 use super::*;
166
167 pub fn serialize<S: Serializer>(
168 value: &Option<Decimal>,
169 serializer: S,
170 ) -> Result<S::Ok, S::Error> {
171 match value {
172 Some(value) => serializer.collect_str(value),
173 None => serializer.serialize_str(""),
174 }
175 }
176
177 pub fn deserialize<'de, D: Deserializer<'de>>(
178 deserializer: D,
179 ) -> Result<Option<Decimal>, D::Error> {
180 let raw = String::deserialize(deserializer)?;
181 if raw.is_empty() {
182 return Ok(None);
183 }
184 raw.parse().map(Some).map_err(de::Error::custom)
185 }
186}
187
188#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
190#[serde(rename_all = "camelCase")]
191#[non_exhaustive]
192pub struct ServerTime {
193 pub server_time: u64,
195}
196
197#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
199#[serde(rename_all = "camelCase")]
200#[non_exhaustive]
201pub struct ExchangeInfo {
202 pub timezone: String,
204 pub server_time: u64,
206 pub futures_type: String,
208 pub rate_limits: Vec<RateLimit>,
210 pub exchange_filters: Vec<Filter>,
212 pub assets: Vec<AssetInfo>,
214 pub symbols: Vec<SymbolInfo>,
216}
217
218#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
220#[serde(rename_all = "camelCase")]
221#[non_exhaustive]
222pub struct RateLimit {
223 pub rate_limit_type: String,
225 pub interval: String,
227 pub interval_num: u32,
229 pub limit: u32,
231}
232
233#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
235#[serde(rename_all = "camelCase")]
236#[non_exhaustive]
237pub struct AssetInfo {
238 pub asset: String,
240 pub margin_available: bool,
242 #[serde(with = "rust_decimal::serde::str")]
244 pub auto_asset_exchange: Decimal,
245}
246
247#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
249#[serde(rename_all = "camelCase")]
250#[non_exhaustive]
251pub struct SymbolInfo {
252 pub symbol: String,
254 pub pair: String,
256 pub contract_type: ContractType,
258 pub delivery_date: u64,
260 pub onboard_date: u64,
262 pub status: SymbolStatus,
264 #[serde(with = "rust_decimal::serde::str")]
266 pub maint_margin_percent: Decimal,
267 #[serde(with = "rust_decimal::serde::str")]
269 pub required_margin_percent: Decimal,
270 pub base_asset: String,
272 pub quote_asset: String,
274 pub margin_asset: String,
276 pub price_precision: u32,
278 pub quantity_precision: u32,
280 pub base_asset_precision: u32,
282 pub quote_precision: u32,
284 pub underlying_type: UnderlyingType,
286 pub underlying_sub_type: Vec<String>,
288 #[serde(with = "rust_decimal::serde::str")]
290 pub trigger_protect: Decimal,
291 #[serde(with = "rust_decimal::serde::str")]
293 pub liquidation_fee: Decimal,
294 #[serde(with = "rust_decimal::serde::str")]
296 pub market_take_bound: Decimal,
297 pub max_move_order_limit: u32,
299 pub filters: Vec<Filter>,
301 pub order_types: Vec<String>,
303 pub time_in_force: Vec<String>,
305 pub permission_sets: Vec<String>,
307}
308
309#[derive(Debug, Clone, PartialEq)]
316#[non_exhaustive]
317pub enum Filter {
318 #[non_exhaustive]
320 PriceFilter {
321 min_price: Decimal,
323 max_price: Decimal,
325 tick_size: Decimal,
327 },
328 #[non_exhaustive]
330 LotSize {
331 min_qty: Decimal,
333 max_qty: Decimal,
335 step_size: Decimal,
337 },
338 #[non_exhaustive]
340 MarketLotSize {
341 min_qty: Decimal,
343 max_qty: Decimal,
345 step_size: Decimal,
347 },
348 #[non_exhaustive]
350 MaxNumOrders {
351 limit: u32,
353 },
354 #[non_exhaustive]
356 MinNotional {
357 notional: Decimal,
359 },
360 #[non_exhaustive]
362 PercentPrice {
363 multiplier_up: Decimal,
365 multiplier_down: Decimal,
367 multiplier_decimal: Decimal,
369 },
370 #[non_exhaustive]
372 PositionRiskControl {
373 position_control_side: String,
375 },
376 Other {
378 filter_type: String,
380 raw: Value,
382 },
383}
384
385mod filter_wire {
387 use super::*;
388
389 #[derive(Serialize, Deserialize)]
390 #[serde(rename_all = "camelCase")]
391 pub struct Price {
392 #[serde(with = "rust_decimal::serde::str")]
393 pub min_price: Decimal,
394 #[serde(with = "rust_decimal::serde::str")]
395 pub max_price: Decimal,
396 #[serde(with = "rust_decimal::serde::str")]
397 pub tick_size: Decimal,
398 }
399
400 #[derive(Serialize, Deserialize)]
401 #[serde(rename_all = "camelCase")]
402 pub struct Lot {
403 #[serde(with = "rust_decimal::serde::str")]
404 pub min_qty: Decimal,
405 #[serde(with = "rust_decimal::serde::str")]
406 pub max_qty: Decimal,
407 #[serde(with = "rust_decimal::serde::str")]
408 pub step_size: Decimal,
409 }
410
411 #[derive(Serialize, Deserialize)]
412 pub struct MaxNumOrders {
413 pub limit: u32,
414 }
415
416 #[derive(Serialize, Deserialize)]
417 pub struct MinNotional {
418 #[serde(with = "rust_decimal::serde::str")]
419 pub notional: Decimal,
420 }
421
422 #[derive(Serialize, Deserialize)]
423 #[serde(rename_all = "camelCase")]
424 pub struct PercentPrice {
425 #[serde(with = "rust_decimal::serde::str")]
426 pub multiplier_up: Decimal,
427 #[serde(with = "rust_decimal::serde::str")]
428 pub multiplier_down: Decimal,
429 #[serde(with = "rust_decimal::serde::str")]
430 pub multiplier_decimal: Decimal,
431 }
432
433 #[derive(Serialize, Deserialize)]
434 #[serde(rename_all = "camelCase")]
435 pub struct PositionRiskControl {
436 pub position_control_side: String,
437 }
438}
439
440impl<'de> Deserialize<'de> for Filter {
441 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
442 use filter_wire as wire;
443
444 fn parse<T: DeserializeOwned, E: de::Error>(raw: Value) -> Result<T, E> {
445 serde_json::from_value(raw).map_err(E::custom)
446 }
447
448 let raw = Value::deserialize(deserializer)?;
449 let filter_type = raw
450 .get("filterType")
451 .and_then(Value::as_str)
452 .ok_or_else(|| de::Error::custom("a filter without a string filterType"))?
453 .to_owned();
454 Ok(match filter_type.as_str() {
455 "PRICE_FILTER" => {
456 let f: wire::Price = parse(raw)?;
457 Self::PriceFilter {
458 min_price: f.min_price,
459 max_price: f.max_price,
460 tick_size: f.tick_size,
461 }
462 }
463 "LOT_SIZE" => {
464 let f: wire::Lot = parse(raw)?;
465 Self::LotSize {
466 min_qty: f.min_qty,
467 max_qty: f.max_qty,
468 step_size: f.step_size,
469 }
470 }
471 "MARKET_LOT_SIZE" => {
472 let f: wire::Lot = parse(raw)?;
473 Self::MarketLotSize {
474 min_qty: f.min_qty,
475 max_qty: f.max_qty,
476 step_size: f.step_size,
477 }
478 }
479 "MAX_NUM_ORDERS" => {
480 let f: wire::MaxNumOrders = parse(raw)?;
481 Self::MaxNumOrders { limit: f.limit }
482 }
483 "MIN_NOTIONAL" => {
484 let f: wire::MinNotional = parse(raw)?;
485 Self::MinNotional {
486 notional: f.notional,
487 }
488 }
489 "PERCENT_PRICE" => {
490 let f: wire::PercentPrice = parse(raw)?;
491 Self::PercentPrice {
492 multiplier_up: f.multiplier_up,
493 multiplier_down: f.multiplier_down,
494 multiplier_decimal: f.multiplier_decimal,
495 }
496 }
497 "POSITION_RISK_CONTROL" => {
498 let f: wire::PositionRiskControl = parse(raw)?;
499 Self::PositionRiskControl {
500 position_control_side: f.position_control_side,
501 }
502 }
503 _ => Self::Other { filter_type, raw },
504 })
505 }
506}
507
508impl Serialize for Filter {
509 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
510 use filter_wire as wire;
511
512 fn tagged<T: Serialize, E: serde::ser::Error>(
513 filter_type: &str,
514 body: &T,
515 ) -> Result<Value, E> {
516 let mut value = serde_json::to_value(body).map_err(E::custom)?;
517 if let Value::Object(map) = &mut value {
518 map.insert(
519 "filterType".to_owned(),
520 Value::String(filter_type.to_owned()),
521 );
522 }
523 Ok(value)
524 }
525
526 let value = match self {
527 Self::PriceFilter {
528 min_price,
529 max_price,
530 tick_size,
531 } => tagged(
532 "PRICE_FILTER",
533 &wire::Price {
534 min_price: *min_price,
535 max_price: *max_price,
536 tick_size: *tick_size,
537 },
538 )?,
539 Self::LotSize {
540 min_qty,
541 max_qty,
542 step_size,
543 } => tagged(
544 "LOT_SIZE",
545 &wire::Lot {
546 min_qty: *min_qty,
547 max_qty: *max_qty,
548 step_size: *step_size,
549 },
550 )?,
551 Self::MarketLotSize {
552 min_qty,
553 max_qty,
554 step_size,
555 } => tagged(
556 "MARKET_LOT_SIZE",
557 &wire::Lot {
558 min_qty: *min_qty,
559 max_qty: *max_qty,
560 step_size: *step_size,
561 },
562 )?,
563 Self::MaxNumOrders { limit } => {
564 tagged("MAX_NUM_ORDERS", &wire::MaxNumOrders { limit: *limit })?
565 }
566 Self::MinNotional { notional } => tagged(
567 "MIN_NOTIONAL",
568 &wire::MinNotional {
569 notional: *notional,
570 },
571 )?,
572 Self::PercentPrice {
573 multiplier_up,
574 multiplier_down,
575 multiplier_decimal,
576 } => tagged(
577 "PERCENT_PRICE",
578 &wire::PercentPrice {
579 multiplier_up: *multiplier_up,
580 multiplier_down: *multiplier_down,
581 multiplier_decimal: *multiplier_decimal,
582 },
583 )?,
584 Self::PositionRiskControl {
585 position_control_side,
586 } => tagged(
587 "POSITION_RISK_CONTROL",
588 &wire::PositionRiskControl {
589 position_control_side: position_control_side.clone(),
590 },
591 )?,
592 Self::Other { raw, .. } => raw.clone(),
593 };
594 value.serialize(serializer)
595 }
596}
597
598#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
601#[serde(rename_all = "camelCase")]
602#[non_exhaustive]
603pub struct FundingInfo {
604 pub symbol: String,
607 #[serde(with = "rust_decimal::serde::str")]
609 pub adjusted_funding_rate_cap: Decimal,
610 #[serde(with = "rust_decimal::serde::str")]
612 pub adjusted_funding_rate_floor: Decimal,
613 pub funding_interval_hours: u32,
615 pub disclaimer: bool,
617 pub update_time: Option<u64>,
620}
621
622#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
624#[serde(rename_all = "camelCase")]
625#[non_exhaustive]
626pub struct Ticker24h {
627 pub symbol: String,
629 #[serde(with = "rust_decimal::serde::str")]
631 pub price_change: Decimal,
632 #[serde(with = "rust_decimal::serde::str")]
634 pub price_change_percent: Decimal,
635 #[serde(with = "rust_decimal::serde::str")]
637 pub weighted_avg_price: Decimal,
638 #[serde(with = "rust_decimal::serde::str")]
640 pub last_price: Decimal,
641 #[serde(with = "rust_decimal::serde::str")]
643 pub last_qty: Decimal,
644 #[serde(with = "rust_decimal::serde::str")]
646 pub open_price: Decimal,
647 #[serde(with = "rust_decimal::serde::str")]
649 pub high_price: Decimal,
650 #[serde(with = "rust_decimal::serde::str")]
652 pub low_price: Decimal,
653 #[serde(with = "rust_decimal::serde::str")]
655 pub volume: Decimal,
656 #[serde(with = "rust_decimal::serde::str")]
658 pub quote_volume: Decimal,
659 pub open_time: u64,
661 pub close_time: u64,
663 pub first_id: i64,
665 pub last_id: i64,
667 pub count: u64,
669}
670
671#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
673#[serde(rename_all = "camelCase")]
674#[non_exhaustive]
675pub struct PremiumIndex {
676 pub symbol: String,
678 #[serde(with = "rust_decimal::serde::str")]
680 pub mark_price: Decimal,
681 #[serde(with = "rust_decimal::serde::str")]
683 pub index_price: Decimal,
684 #[serde(with = "rust_decimal::serde::str")]
686 pub estimated_settle_price: Decimal,
687 #[serde(with = "rust_decimal::serde::str")]
689 pub last_funding_rate: Decimal,
690 #[serde(with = "rust_decimal::serde::str")]
692 pub interest_rate: Decimal,
693 pub next_funding_time: u64,
695 pub time: u64,
697}
698
699#[derive(Debug, Clone, PartialEq, Eq)]
704#[non_exhaustive]
705pub struct Kline {
706 pub open_time: u64,
708 pub open: Decimal,
710 pub high: Decimal,
712 pub low: Decimal,
714 pub close: Decimal,
716 pub volume: Decimal,
718 pub close_time: u64,
720 pub quote_volume: Decimal,
722 pub trade_count: u64,
724 pub taker_buy_base_volume: Decimal,
726 pub taker_buy_quote_volume: Decimal,
728}
729
730impl<'de> Deserialize<'de> for Kline {
731 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
732 struct KlineVisitor;
733
734 impl<'de> Visitor<'de> for KlineVisitor {
735 type Value = Kline;
736
737 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
738 f.write_str("a kline array of at least 11 elements")
739 }
740
741 fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Kline, A::Error> {
742 const WHAT: &str = "a kline array of at least 11 elements";
743 let kline = Kline {
744 open_time: element(&mut seq, 0, WHAT)?,
745 open: element::<DecimalStr, _>(&mut seq, 1, WHAT)?.0,
746 high: element::<DecimalStr, _>(&mut seq, 2, WHAT)?.0,
747 low: element::<DecimalStr, _>(&mut seq, 3, WHAT)?.0,
748 close: element::<DecimalStr, _>(&mut seq, 4, WHAT)?.0,
749 volume: element::<DecimalStr, _>(&mut seq, 5, WHAT)?.0,
750 close_time: element(&mut seq, 6, WHAT)?,
751 quote_volume: element::<DecimalStr, _>(&mut seq, 7, WHAT)?.0,
752 trade_count: element(&mut seq, 8, WHAT)?,
753 taker_buy_base_volume: element::<DecimalStr, _>(&mut seq, 9, WHAT)?.0,
754 taker_buy_quote_volume: element::<DecimalStr, _>(&mut seq, 10, WHAT)?.0,
755 };
756 drain(&mut seq)?;
757 Ok(kline)
758 }
759 }
760
761 deserializer.deserialize_seq(KlineVisitor)
762 }
763}
764
765impl Serialize for Kline {
766 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
767 let mut seq = serializer.serialize_seq(Some(11))?;
768 seq.serialize_element(&self.open_time)?;
769 for value in [&self.open, &self.high, &self.low, &self.close, &self.volume] {
770 seq.serialize_element(&value.to_string())?;
771 }
772 seq.serialize_element(&self.close_time)?;
773 seq.serialize_element(&self.quote_volume.to_string())?;
774 seq.serialize_element(&self.trade_count)?;
775 seq.serialize_element(&self.taker_buy_base_volume.to_string())?;
776 seq.serialize_element(&self.taker_buy_quote_volume.to_string())?;
777 seq.end()
778 }
779}
780
781#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
783#[serde(rename_all = "camelCase")]
784#[non_exhaustive]
785pub struct FundingRate {
786 pub symbol: String,
788 pub funding_time: u64,
790 #[serde(with = "rust_decimal::serde::str")]
792 pub funding_rate: Decimal,
793 #[serde(with = "decimal_or_empty")]
796 pub mark_price: Option<Decimal>,
797 pub rate_type: String,
799}
800
801#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
803#[serde(rename_all = "camelCase")]
804#[non_exhaustive]
805pub struct OpenInterest {
806 pub symbol: String,
808 #[serde(with = "rust_decimal::serde::str")]
810 pub open_interest: Decimal,
811 pub time: u64,
813}
814
815#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
818#[non_exhaustive]
819pub struct AggTrade {
820 #[serde(rename = "a")]
822 pub id: u64,
823 #[serde(rename = "p", with = "rust_decimal::serde::str")]
825 pub price: Decimal,
826 #[serde(rename = "q", with = "rust_decimal::serde::str")]
828 pub quantity: Decimal,
829 #[serde(rename = "nq", with = "rust_decimal::serde::str")]
832 pub normal_quantity: Decimal,
833 #[serde(rename = "f")]
835 pub first_trade_id: u64,
836 #[serde(rename = "l")]
838 pub last_trade_id: u64,
839 #[serde(rename = "T")]
841 pub time: u64,
842 #[serde(rename = "m")]
844 pub is_buyer_maker: bool,
845}
846
847#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
849#[non_exhaustive]
850pub struct Depth {
851 #[serde(rename = "lastUpdateId")]
853 pub last_update_id: u64,
854 #[serde(rename = "E")]
856 pub event_time: u64,
857 #[serde(rename = "T")]
859 pub transaction_time: u64,
860 pub bids: Vec<Level>,
862 pub asks: Vec<Level>,
864}
865
866#[derive(Debug, Clone, Copy, PartialEq, Eq)]
868#[non_exhaustive]
869pub struct Level {
870 pub price: Decimal,
872 pub quantity: Decimal,
874}
875
876impl Level {
877 pub fn new(price: Decimal, quantity: Decimal) -> Self {
879 Self { price, quantity }
880 }
881}
882
883impl<'de> Deserialize<'de> for Level {
884 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
885 struct LevelVisitor;
886
887 impl<'de> Visitor<'de> for LevelVisitor {
888 type Value = Level;
889
890 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
891 f.write_str("a [price, quantity] array")
892 }
893
894 fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Level, A::Error> {
895 const WHAT: &str = "a [price, quantity] array";
896 let level = Level {
897 price: element::<DecimalStr, _>(&mut seq, 0, WHAT)?.0,
898 quantity: element::<DecimalStr, _>(&mut seq, 1, WHAT)?.0,
899 };
900 drain(&mut seq)?;
901 Ok(level)
902 }
903 }
904
905 deserializer.deserialize_seq(LevelVisitor)
906 }
907}
908
909impl Serialize for Level {
910 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
911 let mut seq = serializer.serialize_seq(Some(2))?;
912 seq.serialize_element(&self.price.to_string())?;
913 seq.serialize_element(&self.quantity.to_string())?;
914 seq.end()
915 }
916}
917
918#[cfg(test)]
919mod tests {
920 use super::*;
921
922 #[test]
923 fn a_refused_symbol_and_an_unknown_spelling_are_invalid_requests() {
924 use polyoxide_venue::{Class, Classify, UnknownVariant};
925
926 let symbol = Symbol::new("BTC USDT").unwrap_err();
927 assert_eq!(symbol.class(), Class::InvalidRequest);
928 assert!(symbol.is_fault());
929 assert_eq!(symbol.retry_after(), None);
930 assert!(!symbol.is_retriable());
931
932 let spelling = UnknownVariant {
933 type_name: "Interval",
934 value: "2m".into(),
935 };
936 assert_eq!(spelling.class(), Class::InvalidRequest);
937 assert!(spelling.is_fault());
938 assert_eq!(spelling.retry_after(), None);
939 assert!(!spelling.is_retriable());
940 }
941
942 #[test]
943 fn symbols_take_letters_digits_and_underscores_in_any_script() {
944 for good in ["BTCUSDT", "BTCUSDT_261225", "币安人生USDT", "1000PEPEUSDT"] {
945 assert_eq!(Symbol::new(good).unwrap().as_str(), good);
946 }
947 for bad in [
948 "",
949 "BTC USDT",
950 "BTC@USDT",
951 "BTC/USDT",
952 "BTC-USDT",
953 &"A".repeat(33),
954 ] {
955 assert_eq!(
956 Symbol::new(bad),
957 Err(InvalidSymbol(bad.to_owned())),
958 "{bad:?}"
959 );
960 }
961 assert!(Symbol::new("A".repeat(32)).is_ok());
962 }
963
964 #[test]
965 fn ascii_letters_are_uppercased_and_nothing_else_changes() {
966 assert_eq!(
967 Symbol::new("btcusdt").unwrap(),
968 Symbol::new("BTCUSDT").unwrap()
969 );
970 assert_eq!(
971 Symbol::new("btcusdt_261225").unwrap().as_str(),
972 "BTCUSDT_261225"
973 );
974 assert_eq!(
975 Symbol::new("币安人生usdt").unwrap().as_str(),
976 "币安人生USDT"
977 );
978 }
979
980 #[test]
981 fn symbols_are_measured_in_characters_and_only_ascii_changes_case() {
982 assert_eq!(Symbol::new("straße").unwrap().as_str(), "STRAßE");
984 assert!(Symbol::new("币".repeat(32)).is_ok());
985 assert!(Symbol::new("币".repeat(33)).is_err());
986 }
987
988 #[test]
989 fn wire_spellings_are_binance_s() {
990 let intervals: Vec<&str> = Interval::ALL.iter().map(|i| i.as_str()).collect();
993 assert_eq!(
994 intervals,
995 [
996 "1m", "3m", "5m", "15m", "30m", "1h", "2h", "4h", "6h", "8h", "12h", "1d", "3d",
997 "1w", "1M"
998 ]
999 );
1000 let limits: Vec<&str> = DepthLimit::ALL.iter().map(|l| l.as_str()).collect();
1001 assert_eq!(limits, ["5", "10", "20", "50", "100", "500", "1000"]);
1002 }
1003
1004 #[test]
1005 fn every_interval_round_trips_its_wire_spelling() {
1006 assert_eq!(Interval::ALL.len(), 15);
1007 for interval in Interval::ALL {
1008 assert_eq!(interval.as_str().parse::<Interval>(), Ok(*interval));
1009 }
1010 assert!("1s".parse::<Interval>().is_err(), "this host refuses 1s");
1011 }
1012
1013 #[test]
1014 fn an_unknown_contract_type_is_kept_verbatim() {
1015 let parsed: ContractType = serde_json::from_str(r#""NEXT_DECADE""#).unwrap();
1016 assert_eq!(parsed, ContractType::Other("NEXT_DECADE".to_owned()));
1017 assert_eq!(serde_json::to_string(&parsed).unwrap(), r#""NEXT_DECADE""#);
1018 let known: ContractType = serde_json::from_str(r#""TRADIFI_PERPETUAL""#).unwrap();
1019 assert_eq!(known, ContractType::TradifiPerpetual);
1020 }
1021
1022 #[test]
1023 fn an_unseen_filter_type_is_kept_and_the_known_ones_parse() {
1024 let filters: Vec<Filter> = serde_json::from_str(
1025 r#"[{"filterType":"PRICE_FILTER","minPrice":"261.10","maxPrice":"809484","tickSize":"0.10"},
1026 {"filterType":"MAX_NUM_ICEBERG_ORDERS","maxNumIcebergOrders":5}]"#,
1027 )
1028 .unwrap();
1029 assert!(
1030 matches!(&filters[0], Filter::PriceFilter { tick_size, .. } if *tick_size == Decimal::new(10, 2))
1031 );
1032 let Filter::Other { filter_type, raw } = &filters[1] else {
1033 panic!("an unseen filter type must not fail the response");
1034 };
1035 assert_eq!(filter_type, "MAX_NUM_ICEBERG_ORDERS");
1036 assert_eq!(raw["maxNumIcebergOrders"], 5);
1037 let again: Vec<Value> =
1038 serde_json::from_value(serde_json::to_value(&filters).unwrap()).unwrap();
1039 assert_eq!(again[0]["filterType"], "PRICE_FILTER");
1040 assert_eq!(again[1]["maxNumIcebergOrders"], 5);
1041 }
1042
1043 #[test]
1044 fn a_known_filter_with_a_missing_field_fails_loudly() {
1045 let err = serde_json::from_str::<Filter>(r#"{"filterType":"LOT_SIZE","minQty":"0.001"}"#)
1046 .unwrap_err();
1047 assert!(err.to_string().contains("maxQty"), "{err}");
1048 }
1049
1050 #[test]
1051 fn a_kline_drops_the_ignored_element_and_tolerates_more() {
1052 let wire = r#"[1791354000000,"84125.70","84125.70","84099.90","84106.90","214.561",1791354059999,"18046628.85200",2912,"38.078","3202701.86390","0","extra"]"#;
1053 let kline: Kline = serde_json::from_str(wire).unwrap();
1054 assert_eq!(kline.close, "84106.90".parse::<Decimal>().unwrap());
1055 assert_eq!(kline.trade_count, 2912);
1056 assert_eq!(
1057 serde_json::to_string(&kline).unwrap(),
1058 r#"[1791354000000,"84125.70","84125.70","84099.90","84106.90","214.561",1791354059999,"18046628.85200",2912,"38.078","3202701.86390"]"#
1059 );
1060 assert!(serde_json::from_str::<Kline>("[1791354000000]").is_err());
1061 }
1062
1063 #[test]
1064 fn a_level_keeps_every_digit() {
1065 let level: Level = serde_json::from_str(r#"["84141.123456789012345","14.663"]"#).unwrap();
1067 assert_eq!(level.price.to_string(), "84141.123456789012345");
1068 assert_eq!(
1069 serde_json::to_string(&level).unwrap(),
1070 r#"["84141.123456789012345","14.663"]"#
1071 );
1072 }
1073
1074 #[test]
1075 fn an_old_funding_rate_has_no_mark_price() {
1076 let old: FundingRate = serde_json::from_str(
1078 r#"{"symbol":"BTCUSDT","fundingTime":1568102400000,"fundingRate":"0.00010000","markPrice":"","rateType":"Regular"}"#,
1079 )
1080 .unwrap();
1081 assert_eq!(old.mark_price, None);
1082 assert_eq!(serde_json::to_value(&old).unwrap()["markPrice"], "");
1083
1084 let new: FundingRate = serde_json::from_str(
1085 r#"{"symbol":"BTCUSDT","fundingTime":1791273600001,"fundingRate":"-0.00001592","markPrice":"85514.00007496","rateType":"Regular"}"#,
1086 )
1087 .unwrap();
1088 assert_eq!(new.mark_price, Some("85514.00007496".parse().unwrap()));
1089 }
1090}