v_utils/trades/
pair.rs

1use derive_more::{Deref, DerefMut};
2use eyre::Report;
3use serde::{Deserializer, Serialize, Serializer};
4
5#[derive(Clone, Default, Copy, PartialEq, Eq, Hash, Deref, DerefMut, PartialOrd, Ord)]
6pub struct Asset(pub [u8; 16]);
7impl Asset {
8	pub fn new<S: AsRef<str>>(s: S) -> Self {
9		let s = s.as_ref().to_uppercase();
10		let mut bytes = [0; 16];
11		bytes[..s.len()].copy_from_slice(s.as_bytes());
12		Self(bytes)
13	}
14
15	fn fmt(&self) -> &str {
16		std::str::from_utf8(&self.0).unwrap().trim_end_matches('\0')
17	}
18
19	pub fn as_str(&self) -> &str {
20		self.fmt()
21	}
22}
23//HACK: should implement `pad`, but rust is broken (or skill issue (upd: definitely broken)). Whatever the case, doing `f.pad(s)` on the same output breaks things downstream (no clue why).
24impl std::fmt::Display for Asset {
25	fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
26		write!(f, "{}", self.fmt())
27	}
28}
29impl std::fmt::Debug for Asset {
30	fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
31		write!(f, "{}", self.fmt())
32	}
33}
34impl From<&str> for Asset {
35	fn from(s: &str) -> Self {
36		Self::new(s)
37	}
38}
39impl From<String> for Asset {
40	fn from(s: String) -> Self {
41		Self::new(s)
42	}
43}
44impl AsRef<str> for Asset {
45	fn as_ref(&self) -> &str {
46		self.fmt()
47	}
48}
49impl PartialEq<str> for Asset {
50	fn eq(&self, other: &str) -> bool {
51		self.fmt() == other
52	}
53}
54impl PartialEq<&str> for Asset {
55	fn eq(&self, other: &&str) -> bool {
56		&self.fmt() == other
57	}
58}
59
60#[derive(Clone, Debug, Default, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
61pub struct Pair {
62	base: Asset,
63	quote: Asset,
64}
65impl Pair {
66	pub fn new<S: Into<Asset>>(base: S, quote: S) -> Self {
67		Self {
68			base: base.into(),
69			quote: quote.into(),
70		}
71	}
72
73	pub fn is_usdt(&self) -> bool {
74		self.quote == "USDT" && self.base != "BTCST" /*Binance thing*/
75	}
76
77	pub fn base(&self) -> &Asset {
78		&self.base
79	}
80
81	pub fn quote(&self) -> &Asset {
82		&self.quote
83	}
84
85	// Exchange-specific {{{
86	pub fn fmt_binance(&self) -> String {
87		format!("{}{}", self.base, self.quote)
88	}
89
90	pub fn fmt_bybit(&self) -> String {
91		format!("{}{}", self.base, self.quote)
92	}
93
94	pub fn fmt_mexc(&self) -> String {
95		format!("{}_{}", self.base, self.quote)
96	}
97	//,}}}
98}
99impl<A: Into<Asset>> From<(A, A)> for Pair {
100	fn from((base, quote): (A, A)) -> Self {
101		Self::new(base, quote)
102	}
103}
104//HACK: should implement `pad`, but rust is broken (or skill issue). Whatever the case, doing `f.pad(s)` on the same output breaks things downstream (no clue why).
105impl std::fmt::Display for Pair {
106	fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
107		write!(f, "{}-{}", self.base, self.quote)
108	}
109}
110
111impl<'de> serde::Deserialize<'de> for Pair {
112	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
113	where
114		D: Deserializer<'de>, {
115		let s = String::deserialize(deserializer)?;
116		s.parse().map_err(serde::de::Error::custom)
117	}
118}
119impl Serialize for Pair {
120	fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
121	where
122		S: Serializer, {
123		self.to_string().serialize(serializer)
124	}
125}
126
127#[derive(thiserror::Error, Debug)]
128#[error("Invalid pair format '{provided_str}'. Expected two assets separated by one of: [{}]", allowed_delimiters.join(" "))]
129pub struct InvalidPairError {
130	provided_str: String,
131	allowed_delimiters: Vec<String>,
132}
133impl InvalidPairError {
134	pub fn new<S: Into<String>>(provided_str: &str, allowed_delimiters: impl IntoIterator<Item = S>) -> Self {
135		Self {
136			provided_str: provided_str.to_owned(),
137			allowed_delimiters: allowed_delimiters.into_iter().map(Into::into).collect(),
138		}
139	}
140}
141
142/// function to prevent human error in the order of the prefixes, because I know sooner or later I'll mess it up. Will return false if say "WETH" is found _after_ "ETH"
143///HACK: couldn't figure out how to do this at compile time
144#[doc(hidden)]
145fn check_prefix_order<const N: usize>(arr: [&str; N]) -> eyre::Result<()> {
146	for i in 0..N {
147		for j in (i + 1)..N {
148			if arr[i].len() < arr[j].len() && arr[j].ends_with(arr[i]) {
149				eyre::bail!("{} is a suffix of {}", arr[i], arr[j]);
150			}
151		}
152	}
153	Ok(())
154}
155
156impl std::str::FromStr for Pair {
157	type Err = Report;
158
159	fn from_str(s: &str) -> Result<Self, Self::Err> {
160		let delimiters = [',', '-', '_', '/'];
161		let currencies = [
162			"EURI", "EUR", "USD", "GBP", "USDP", "USDS", "PLN", "RON", "CZK", "TRY", "JPY", "BRL", "RUB", "AUD", "NGN", "MXN", "COP", "ARS", "BKRW", "IDRT", "UAH", "BIDR", "BVND", "ZAR",
163		];
164		let crypto = ["USDT", "USDC", "UST", "BTC", "WETH", "ETH", "BNB", "SOL", "XRP", "PAX", "DAI", "VAI", "DOGE", "DOT", "TRX"];
165		if let Err(e) = check_prefix_order(currencies) {
166			unreachable!("Invalid prefix order, I messed up bad: {e}");
167		}
168		if let Err(e) = check_prefix_order(crypto) {
169			unreachable!("Invalid prefix order, I messed up bad: {e}");
170		}
171		let recognized_quotes = [currencies.as_slice(), crypto.as_slice()].concat();
172
173		for delimiter in delimiters {
174			if s.contains(delimiter) {
175				let parts: Vec<_> = s.split(delimiter).map(str::trim).filter(|s| !s.is_empty()).collect();
176				if parts.len() == 2 {
177					return Ok(Self::new(parts[0], parts[1]));
178				}
179				return Err(InvalidPairError::new(s, delimiters.iter().map(|c| c.to_string())).into());
180			}
181		}
182
183		if let Some(quote) = recognized_quotes.iter().find(|q| s.ends_with(*q)) {
184			let base_len = s.len() - quote.len();
185			if base_len > 0 {
186				let base = &s[..base_len];
187				return Ok(Self::new(base, *quote));
188			}
189		}
190
191		Err(InvalidPairError::new(s, delimiters.iter().map(|c| c.to_string())).into())
192	}
193}
194impl TryFrom<&str> for Pair {
195	type Error = Report;
196
197	fn try_from(s: &str) -> Result<Self, Self::Error> {
198		s.parse()
199	}
200}
201impl TryFrom<String> for Pair {
202	type Error = Report;
203
204	fn try_from(s: String) -> Result<Self, Self::Error> {
205		s.parse()
206	}
207}
208impl From<Pair> for String {
209	fn from(pair: Pair) -> Self {
210		pair.to_string()
211	}
212}
213
214impl PartialEq<Pair> for &str {
215	fn eq(&self, other: &Pair) -> bool {
216		Pair::try_from(*self).expect("provided string can't be converted to `Pair` automatically") == *other // important not to cast `to_string` on `Pair` instance here as it will break (nightmare to debug) if I change `Display` impl
217	}
218}
219
220impl PartialEq<Pair> for str {
221	fn eq(&self, other: &Pair) -> bool {
222		self == other
223	}
224}
225
226#[cfg(test)]
227mod tests {
228	use super::*;
229
230	#[test]
231	fn parse_pairs() {
232		assert_eq!("BTC-USD".parse::<Pair>().unwrap(), Pair::new("BTC", "USD"));
233		assert_eq!("ETH,USD".parse::<Pair>().unwrap(), Pair::new("ETH", "USD"));
234		assert_eq!("SOL_USDT".parse::<Pair>().unwrap(), Pair::new("SOL", "USDT"));
235		assert_eq!("XRP/USDC".parse::<Pair>().unwrap(), Pair::new("XRP", "USDC"));
236		assert_eq!("btc - usd".parse::<Pair>().unwrap(), Pair::new("BTC", "USD"));
237		assert_eq!("DOGEUSDT".parse::<Pair>().unwrap(), Pair::new("DOGE", "USDT"));
238		assert_eq!(Pair::from(("ADA", "USDT")), Pair::new("ADA", "USDT"));
239
240		assert!("something".parse::<Pair>().is_err());
241		assert!("".parse::<Pair>().is_err());
242		assert!("BTC".parse::<Pair>().is_err());
243		assert!("BTC-".parse::<Pair>().is_err());
244		assert!("-USD".parse::<Pair>().is_err());
245	}
246
247	#[test]
248	fn display_pairs() {
249		assert_eq!(Pair::new("BTC", "USDT").to_string(), "BTC-USDT");
250	}
251}